toolroll 0.6.0 → 0.8.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 +413 -0
- package/README.md +15 -0
- package/dist/agent-onboard.d.ts +106 -0
- package/dist/agent-onboard.js +282 -0
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +23 -23
- package/dist/browser-shell.js +5 -3
- package/dist/browser-workspace.d.ts +45 -0
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +8 -1
- package/dist/control-setup.d.ts +4 -4
- package/dist/daemon.d.ts +4 -0
- package/dist/daemon.js +28 -4
- package/dist/desktop-host.js +2 -2
- package/dist/desktop-update.d.ts +7 -1
- package/dist/desktop-update.js +39 -22
- package/dist/evidence.d.ts +24 -0
- package/dist/evidence.js +103 -5
- package/dist/first-run.d.ts +54 -0
- package/dist/first-run.js +101 -0
- package/dist/install-method.d.ts +17 -0
- package/dist/install-method.js +65 -0
- package/dist/operate.d.ts +14 -0
- package/dist/operate.js +114 -18
- package/dist/provider-auth.d.ts +33 -8
- package/dist/provider-auth.js +54 -13
- package/dist/releases.d.ts +85 -0
- package/dist/releases.js +238 -0
- package/dist/serve.d.ts +18 -0
- package/dist/serve.js +237 -81
- package/dist/skills.d.ts +2 -0
- package/dist/skills.js +3 -1
- package/dist/store.d.ts +3 -3
- package/dist/store.js +9 -4
- package/dist/surface.js +2 -0
- package/dist/toolroll-update-cli.d.ts +15 -0
- package/dist/toolroll-update-cli.js +155 -0
- package/dist/toolroll-update-ui.d.ts +38 -0
- package/dist/toolroll-update-ui.js +89 -0
- package/dist/toolroll-update.d.ts +235 -0
- package/dist/toolroll-update.js +929 -0
- package/dist/update-notice.d.ts +5 -0
- package/dist/update-notice.js +4 -0
- package/dist/work-index.js +5 -3
- package/package.json +2 -1
package/dist/serve.js
CHANGED
|
@@ -125,7 +125,6 @@ import { openRouterModelsCache, openRouterPickerScript } from "./openrouter-mode
|
|
|
125
125
|
import { checkModels, isNewModel, livePin, modelOptions, modelWords, RUNTIME_TOOLS, runtimeStates, seenModels, setWatch, updateRuntime, watchState } from "./model-catalog.js";
|
|
126
126
|
import { MODELS_CSS, modelsHtml, modelsScript } from "./models-ui.js";
|
|
127
127
|
import { ASSISTANTS, modelChoices, detectPreparation, previewProjectInstructions, addProjectInstructions } from "./setup-guide.js";
|
|
128
|
-
import { projectSkillInstalled } from "./skills.js";
|
|
129
128
|
import { previewSetup, approveSetup } from "./control-setup.js";
|
|
130
129
|
import { controlSetupHtml, setupPreviewHtml, connectionHtml, connectionWords, hiddenFields } from "./control-ui.js";
|
|
131
130
|
import { composerSchedule, scheduleEditorHtml, scheduleEditorScript } from "./task-composer.js";
|
|
@@ -171,6 +170,7 @@ import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, listGithubRepos, i
|
|
|
171
170
|
import { verifiedAuthor, LEAD_THREAD } from "./store.js";
|
|
172
171
|
import { updateRepos, addRepos, removeRepos } from "./repos.js";
|
|
173
172
|
import { run as execRun } from "./exec.js";
|
|
173
|
+
import { findFirstTasks, firstResultWords, firstRunSteps, firstTaskSuggestions, SANDBOX_COMMAND } from "./first-run.js";
|
|
174
174
|
/** A user agent, reduced to safe display words — never echoed raw. */
|
|
175
175
|
function oneLineUa(raw) {
|
|
176
176
|
const text = Array.isArray(raw) ? (raw[0] ?? "") : (raw ?? "");
|
|
@@ -206,6 +206,9 @@ import { BACKUP_EVERY_HOURS, MAX_KEEP, backupFolderOf, backupNow, checkBackupFol
|
|
|
206
206
|
import { prometheusMetrics } from "./metrics.js";
|
|
207
207
|
import { SPEND_CSS, spendCsv, spendHtml } from "./spend-ui.js";
|
|
208
208
|
import { RETENTION_CSS, retentionHtml } from "./retention-ui.js";
|
|
209
|
+
import { UPDATES_CSS, newerThan, updateStepsHtml, updatesHtml, updatesScript } from "./toolroll-update-ui.js";
|
|
210
|
+
import { abandonRuntimeUpdate, launchRuntimeUpdate, markWhatsNewSeen, prepareRuntimeUpdate, requestRuntimeUpdateCancel, runningWorkWords, runtimeUpdateStatus, runtimeUpdateTerminal, currentRuntime } from "./toolroll-update.js";
|
|
211
|
+
import { latestVersionNow } from "./releases.js";
|
|
209
212
|
import { RETENTION_KINDS, lastSweepAt, parsePeriod, retentionPlan } from "./retention.js";
|
|
210
213
|
import { EXPORT_CSS, dataExportHtml } from "./export-ui.js";
|
|
211
214
|
import { buildExport, exportZip } from "./export.js";
|
|
@@ -231,6 +234,10 @@ import { verifyApproverByPassword, verifyApproverStanding } from "./principal.js
|
|
|
231
234
|
import { runMateTurn, MATE_MESSAGE_MAX_CHARS } from "./mate.js";
|
|
232
235
|
import { confirmCoordinatorProposal, confirmMateProposal, dismissCoordinatorProposal, dismissMateProposal } from "./mate-doors.js";
|
|
233
236
|
import { envValue } from "./names.js";
|
|
237
|
+
import { cachedRelease, isNewer, runnerVersions, setUpdateChecks, updateChecksOff } from "./releases.js";
|
|
238
|
+
import { installMethod } from "./install-method.js";
|
|
239
|
+
import { PACKAGE_VERSION } from "./version.js";
|
|
240
|
+
import { updateNoticeWords } from "./update-notice.js";
|
|
234
241
|
const SESSION_COOKIE = "standing-orders_session";
|
|
235
242
|
const BODY_CAP = 16 * 1024;
|
|
236
243
|
// URL encoding can triple UTF-8 bytes. Admit the existing bounded task
|
|
@@ -386,6 +393,32 @@ export function createDecisionServer(options) {
|
|
|
386
393
|
}
|
|
387
394
|
}
|
|
388
395
|
const clock = options.clock ?? (() => new Date());
|
|
396
|
+
const install = installMethod(options.installBin);
|
|
397
|
+
/** What the last daily check found (the cache only, never the network), or nothing while checks are off. */
|
|
398
|
+
const updateFacts = () => {
|
|
399
|
+
if (options.configDir === undefined)
|
|
400
|
+
return null;
|
|
401
|
+
const switched = updateChecksOff(process.env, options.configDir);
|
|
402
|
+
const release = switched.off ? null : cachedRelease(options.configDir);
|
|
403
|
+
return { release, newer: release !== null && isNewer(release.version, PACKAGE_VERSION), on: !switched.off, byEnv: switched.byEnv };
|
|
404
|
+
};
|
|
405
|
+
/** Settings → Updates, for everyone who can open Settings; only an operator may flip the switch. */
|
|
406
|
+
const settingsUpdates = (actor, csrf) => {
|
|
407
|
+
const facts = updateFacts();
|
|
408
|
+
if (facts === null)
|
|
409
|
+
return null;
|
|
410
|
+
const known = new Map(runnerVersions(options.configDir).map(one => [one.runner, one.version]));
|
|
411
|
+
return {
|
|
412
|
+
current: PACKAGE_VERSION,
|
|
413
|
+
latest: facts.release === null ? null : { version: facts.release.version, newer: facts.newer, security: facts.release.security, notes: facts.release.notes, url: facts.release.url },
|
|
414
|
+
updateCommand: install.updateCommand,
|
|
415
|
+
check: { on: facts.on, byEnv: facts.byEnv, canManage: csrf !== "" && store.isInstanceOperator(actor) },
|
|
416
|
+
workers: store.listRunners().filter(one => one.retiredAt === null).map(one => {
|
|
417
|
+
const version = known.get(one.name) ?? null;
|
|
418
|
+
return { name: one.name, version, older: version !== null && isNewer(PACKAGE_VERSION, version) };
|
|
419
|
+
}),
|
|
420
|
+
};
|
|
421
|
+
};
|
|
389
422
|
const workspaceRevision = prepareWorkspaceRevision(store);
|
|
390
423
|
const workspaceIncarnation = randomBytes(16).toString('hex');
|
|
391
424
|
const workspaceValidators = new WorkspaceValidatorCache();
|
|
@@ -1349,6 +1382,7 @@ export function createDecisionServer(options) {
|
|
|
1349
1382
|
const requestFacts = {
|
|
1350
1383
|
theme: pinnedTheme(request.headers.cookie),
|
|
1351
1384
|
accent: pinnedAccent(request.headers.cookie),
|
|
1385
|
+
updateSeen: /(?:^|;\s*)so-update-seen=([0-9A-Za-z.-]{1,60})(?:;|$)/.exec(request.headers.cookie ?? "")?.[1] ?? null,
|
|
1352
1386
|
actor: who.name,
|
|
1353
1387
|
csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
1354
1388
|
returnTo: safeReturn(url.pathname + url.search),
|
|
@@ -1849,7 +1883,7 @@ export function createDecisionServer(options) {
|
|
|
1849
1883
|
})),
|
|
1850
1884
|
cancelledBlockers: cancelled,
|
|
1851
1885
|
gaps: project === null ? [] : computeGaps(store, project, now).filter(gap => gap.unblocks.length > 0).slice(0, 10),
|
|
1852
|
-
wizard:
|
|
1886
|
+
wizard: await firstRunStepsNow(now),
|
|
1853
1887
|
worker: (() => {
|
|
1854
1888
|
// The one fact the inbox must never hide (install review): with
|
|
1855
1889
|
// no worker answering, nothing here will ever build, and every
|
|
@@ -3342,10 +3376,13 @@ export function createDecisionServer(options) {
|
|
|
3342
3376
|
// A failed briefing query must not make the conversation vanish.
|
|
3343
3377
|
}
|
|
3344
3378
|
}
|
|
3379
|
+
// The first run belongs to the lead conversation, for someone who can act on it.
|
|
3380
|
+
const firstRun = focusTask === null && who.role === "approver" ? await chatFirstRun(now, chatProject) : undefined;
|
|
3381
|
+
const withFirstRun = (shown) => firstRun === undefined ? shown : { ...shown, workspace: { ...shown.workspace, firstRun } };
|
|
3345
3382
|
if (enabled.ok && mateSession !== null && principal !== null && !ceilingStale) {
|
|
3346
3383
|
{
|
|
3347
3384
|
const said = takeMateNote(who.session.csrf, mateSession.id);
|
|
3348
|
-
return sendScreen(response, 200, matePage(chromeFor(null, "chat", undefined, "all"), {
|
|
3385
|
+
return sendScreen(response, 200, withFirstRun(matePage(chromeFor(null, "chat", undefined, "all"), {
|
|
3349
3386
|
session: mateSession,
|
|
3350
3387
|
...mateConversationRows(who, principal, focusTask, now, chatProject),
|
|
3351
3388
|
chatProject,
|
|
@@ -3363,10 +3400,10 @@ export function createDecisionServer(options) {
|
|
|
3363
3400
|
now,
|
|
3364
3401
|
resultPanel,
|
|
3365
3402
|
resultRunId: resultRun?.id ?? null,
|
|
3366
|
-
}));
|
|
3403
|
+
})));
|
|
3367
3404
|
}
|
|
3368
3405
|
}
|
|
3369
|
-
return sendScreen(response, 200, chatPage(chromeFor(null, "chat", undefined, "all"), {
|
|
3406
|
+
return sendScreen(response, 200, withFirstRun(chatPage(chromeFor(null, "chat", undefined, "all"), {
|
|
3370
3407
|
settingsOpen: settingsView,
|
|
3371
3408
|
enabled,
|
|
3372
3409
|
pending,
|
|
@@ -3399,7 +3436,7 @@ export function createDecisionServer(options) {
|
|
|
3399
3436
|
resultPanel,
|
|
3400
3437
|
...(enabled.ok && who.role === "approver" ? { mateMint: mateMintCard(who.session.csrf, enabled, focusTask === null ? "/chat" : taskChatHref(focusTask.id)) } : {}),
|
|
3401
3438
|
...(who.role === "approver" ? { coordinatorProposals: coordinatorProposalsSection(coordinatorRows, decisionsFor(store, coordinatorRows), who.session.csrf, now, true, focusTask === null ? null : taskChatHref(focusTask.id)) } : {}),
|
|
3402
|
-
}));
|
|
3439
|
+
})));
|
|
3403
3440
|
}
|
|
3404
3441
|
const chatAck = /^\/chat\/ack\/([0-9]{1,15})$/.exec(url.pathname);
|
|
3405
3442
|
if (chatAck !== null) {
|
|
@@ -3586,6 +3623,21 @@ export function createDecisionServer(options) {
|
|
|
3586
3623
|
const view = { settings: readMonitoring(options.configDir), status: store.monitoringStatus(), head: store.ledgerHeadId(), origin: consoleOrigin(request.headers.host) };
|
|
3587
3624
|
return sendScreen(response, 200, screen("Monitoring", `<p><a href="/settings">Settings</a></p><h1>Monitoring</h1>${monitoringHtml(view, who.session.csrf, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "settings") }));
|
|
3588
3625
|
}
|
|
3626
|
+
// Settings → Updates: the installed version, three ways to update, the update's steps live, then What's new. An instance operator's page.
|
|
3627
|
+
if (url.pathname === "/settings/updates") {
|
|
3628
|
+
const databaseFile = store.databaseFile();
|
|
3629
|
+
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
|
|
3630
|
+
return refuse(response, who, 403, "An instance operator updates Toolroll.", "/settings");
|
|
3631
|
+
const status = runtimeUpdateStatus(dirname(databaseFile));
|
|
3632
|
+
if (url.searchParams.get("fragment") === "steps")
|
|
3633
|
+
return respond(response, 200, "text/html; charset=utf-8", status.journal ? updateStepsHtml(status.journal, status.running) : `<div id="update-live" data-done="1"></div>`);
|
|
3634
|
+
const active = status.journal !== null && !runtimeUpdateTerminal(status.journal.phase);
|
|
3635
|
+
const current = options.updates?.current ?? PACKAGE_VERSION;
|
|
3636
|
+
// While update checks are off the page asks npm nothing until Check now.
|
|
3637
|
+
const checksOff = updateChecksOff(process.env, options.configDir ?? dirname(databaseFile)).off && url.searchParams.get("check") !== "now";
|
|
3638
|
+
const view = { current, latest: active ? { version: current } : checksOff ? { off: true } : await latestReleaseFor(options.updates?.latest), method: options.updates?.method ?? installMethod(), journal: status.journal, running: status.running, whatsNew: status.whatsNew, csrf: who.session.csrf };
|
|
3639
|
+
return sendScreen(response, 200, screen("Updates", `<p><a href="/settings">Settings</a></p><h1>Updates</h1>${updatesHtml(view, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "settings"), ...(active ? { functional: { script: updatesScript(), fetches: true } } : {}) }));
|
|
3640
|
+
}
|
|
3589
3641
|
// v105: Settings → Retention, how long each kind of data is kept. An instance operator's page.
|
|
3590
3642
|
if (url.pathname === "/settings/retention") {
|
|
3591
3643
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
@@ -3763,7 +3815,11 @@ export function createDecisionServer(options) {
|
|
|
3763
3815
|
const googleView = { connected: googleConnected(options.configDir ?? null)?.address ?? null, clientId: google?.clientId ?? "", redirect: origin === null ? null : `${origin}${GOOGLE_CALLBACK}` };
|
|
3764
3816
|
return email === null ? { set: false, host: "", port: 587, secure: false, user: "", from: "", imapHost: "", imapPort: 993, google: googleView }
|
|
3765
3817
|
: { set: true, host: email.host, port: email.port, secure: email.secure, user: email.user, from: email.from, imapHost: email.imap?.host ?? "", imapPort: email.imap?.port ?? 993, google: googleView };
|
|
3766
|
-
})() : null, telegramDeliveryWords(store, loadBotToken(process.env, options.telegramTokenFile)), settingsWorkers(store, now)))
|
|
3818
|
+
})() : null, telegramDeliveryWords(store, loadBotToken(process.env, options.telegramTokenFile)), settingsWorkers(store, now), settingsUpdates(who.name, csrf), (() => {
|
|
3819
|
+
// The installation fact: when the first Ready result arrived, measured from the first account.
|
|
3820
|
+
const at = store.firstSuccessAt(now), since = store.installationStartedAt();
|
|
3821
|
+
return at === null || since === null ? null : firstResultWords(since, at);
|
|
3822
|
+
})()));
|
|
3767
3823
|
}
|
|
3768
3824
|
if (url.pathname === "/push/key") {
|
|
3769
3825
|
// Session-gated: the key is not secret, but strangers get nothing.
|
|
@@ -3803,62 +3859,46 @@ export function createDecisionServer(options) {
|
|
|
3803
3859
|
return refuse(response, who, 404, "There's no page at this address.", "/chat");
|
|
3804
3860
|
}
|
|
3805
3861
|
/**
|
|
3806
|
-
* The first
|
|
3807
|
-
* state on every render, never a stored cursor, and retired
|
|
3808
|
-
* by the first-success installation fact (Codex adoption
|
|
3809
|
-
* finding 14).
|
|
3810
|
-
*
|
|
3811
|
-
* checklist instructs, it never gains authority (finding e).
|
|
3862
|
+
* The first run (adoption track, step 3): three plain steps derived from
|
|
3863
|
+
* live state on every render, never a stored cursor, and retired
|
|
3864
|
+
* PERMANENTLY by the first-success installation fact (Codex adoption
|
|
3865
|
+
* review, finding 14). Each step is either something this console can do,
|
|
3866
|
+
* or the exact command where only the CLI can (finding e).
|
|
3812
3867
|
*/
|
|
3813
|
-
function
|
|
3814
|
-
if (restricted() || store.firstSuccessAt(now) !== null)
|
|
3868
|
+
async function firstRunStepsNow(now) {
|
|
3869
|
+
if (restricted() || store.isDemo() || store.firstSuccessAt(now) !== null)
|
|
3815
3870
|
return null;
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
{
|
|
3848
|
-
done: repos.length > 0 && skillDone === repos.length,
|
|
3849
|
-
title: "teach the repo's agents this queue exists",
|
|
3850
|
-
detail: skillDone > 0
|
|
3851
|
-
? `skill installed${counted(skillDone)}`
|
|
3852
|
-
: `<code>toolroll skills install --repo <path></code> previews; add <code>--yes</code> to write the skill file`,
|
|
3853
|
-
},
|
|
3854
|
-
{
|
|
3855
|
-
done: store.hasAnyWork(),
|
|
3856
|
-
title: "file the first standing order",
|
|
3857
|
-
detail: store.hasAnyWork()
|
|
3858
|
-
? `work is filed — approve its scope and the machine takes it from there`
|
|
3859
|
-
: `start from a template below, capture a one-off task underneath, or browse <a href="/routines">routines</a>`,
|
|
3860
|
-
},
|
|
3861
|
-
];
|
|
3871
|
+
return firstRunSteps({ agentSignedIn: await agentSignedIn(), projects: managedRepos().length, hasTask: store.hasAnyWork(), firstResultAt: null });
|
|
3872
|
+
}
|
|
3873
|
+
/** Whether any coding agent on this computer is signed in: the same
|
|
3874
|
+
* non-spending checks Settings makes, cached briefly. */
|
|
3875
|
+
async function agentSignedIn() {
|
|
3876
|
+
const states = await Promise.all(["claude", "codex"].map(one => connectionCheck(one).then(value => value.state, () => "unverified")));
|
|
3877
|
+
return states.some(state => state === "connected" || state === "key-works" || state === "key-present");
|
|
3878
|
+
}
|
|
3879
|
+
/** First tasks per project, read at most every ten minutes: a `gh`
|
|
3880
|
+
* round trip and a `git grep` never run on every render. */
|
|
3881
|
+
const firstTasks = new Map();
|
|
3882
|
+
function firstTasksFor(repo) {
|
|
3883
|
+
const at = Date.now(), hit = firstTasks.get(repo);
|
|
3884
|
+
if (hit !== undefined && hit.until > at)
|
|
3885
|
+
return hit.value;
|
|
3886
|
+
const value = findFirstTasks(repo, options.firstTaskRunner ?? execRun).catch(() => firstTaskSuggestions({ issues: [], todos: [] }));
|
|
3887
|
+
firstTasks.set(repo, { until: at + 10 * 60_000, value });
|
|
3888
|
+
return value;
|
|
3889
|
+
}
|
|
3890
|
+
/** Chat's first run: the steps, first tasks until the first task exists, and the sandbox while no agent is signed in. */
|
|
3891
|
+
async function chatFirstRun(now, project) {
|
|
3892
|
+
const steps = await firstRunStepsNow(now);
|
|
3893
|
+
if (steps === null)
|
|
3894
|
+
return undefined;
|
|
3895
|
+
const taskFiled = steps.find(one => one.key === "task")?.done ?? true;
|
|
3896
|
+
const repo = project ?? managedRepos()[0] ?? null;
|
|
3897
|
+
return {
|
|
3898
|
+
steps,
|
|
3899
|
+
suggestions: taskFiled || repo === null ? [] : await firstTasksFor(repo),
|
|
3900
|
+
sandbox: steps.find(one => one.key === "agent")?.done === false ? SANDBOX_COMMAND : null,
|
|
3901
|
+
};
|
|
3862
3902
|
}
|
|
3863
3903
|
// ---- fleet chat (v13) ----------------------------------------------------
|
|
3864
3904
|
const chatFetcher = options.chatFetcher ?? fetch;
|
|
@@ -4441,6 +4481,8 @@ export function createDecisionServer(options) {
|
|
|
4441
4481
|
chats,
|
|
4442
4482
|
...(s.refreshSeconds === undefined ? {} : { refreshSeconds: Math.max(5, Math.floor(s.refreshSeconds)) }),
|
|
4443
4483
|
...(s.chrome.signIn === undefined ? {} : { signIn: s.chrome.signIn }),
|
|
4484
|
+
...(s.chrome.update === undefined ? {} : { update: s.chrome.update }),
|
|
4485
|
+
...(extras.firstRun === undefined ? {} : { firstRun: extras.firstRun }),
|
|
4444
4486
|
};
|
|
4445
4487
|
if (requestFacts.workspaceRead) {
|
|
4446
4488
|
const validator = requestFacts.workspaceValidator;
|
|
@@ -4557,6 +4599,15 @@ export function createDecisionServer(options) {
|
|
|
4557
4599
|
settings: true,
|
|
4558
4600
|
...(store.isDemo() ? { demo: true } : {}),
|
|
4559
4601
|
...(() => { const signIn = signInNotices(store); return signIn.length === 0 ? {} : { signIn }; })(),
|
|
4602
|
+
...(() => {
|
|
4603
|
+
// Operators only (only someone at this computer can update it), and gone once this version is dismissed.
|
|
4604
|
+
if (!facts?.csrf || !actor || !store.isInstanceOperator(actor))
|
|
4605
|
+
return {};
|
|
4606
|
+
const update = updateFacts();
|
|
4607
|
+
if (update === null || !update.newer || update.release === null || facts.updateSeen === update.release.version)
|
|
4608
|
+
return {};
|
|
4609
|
+
return { update: { version: update.release.version, security: update.release.security, href: "/settings#updates", dismissHref: "/settings/updates/dismiss" } };
|
|
4610
|
+
})(),
|
|
4560
4611
|
...(liveMode === null || liveModeTerms === null
|
|
4561
4612
|
? {}
|
|
4562
4613
|
: {
|
|
@@ -5754,6 +5805,29 @@ export function createDecisionServer(options) {
|
|
|
5754
5805
|
liftAuthPause(store, provider, "person", who.name, clock());
|
|
5755
5806
|
return redirect(response, "/work");
|
|
5756
5807
|
}
|
|
5808
|
+
// The update notice is dismissed per browser and per version: a newer release shows it again.
|
|
5809
|
+
if (url.pathname === "/settings/updates/dismiss") {
|
|
5810
|
+
const version = body.get("version") ?? "";
|
|
5811
|
+
if (!/^[0-9A-Za-z.-]{1,60}$/.test(version))
|
|
5812
|
+
return refuse(response, who, 400, "Choose a version to dismiss.", "/settings#updates");
|
|
5813
|
+
response.setHeader("Set-Cookie", `so-update-seen=${version}; SameSite=Lax; Path=/; Max-Age=31536000`);
|
|
5814
|
+
if (body.get("quiet") === "1") {
|
|
5815
|
+
response.writeHead(204);
|
|
5816
|
+
response.end();
|
|
5817
|
+
return;
|
|
5818
|
+
}
|
|
5819
|
+
return redirect(response, safeReturn(body.get("return") ?? "/settings#updates"));
|
|
5820
|
+
}
|
|
5821
|
+
// The daily check's switch: an installation setting, so an operator's. (Its own address: POST /settings/updates starts an update.)
|
|
5822
|
+
if (url.pathname === "/settings/updates/checks") {
|
|
5823
|
+
if (options.configDir === undefined)
|
|
5824
|
+
return refuse(response, who, 404, "Updates are not set up on this console.", "/settings");
|
|
5825
|
+
if (!store.isInstanceOperator(who.name))
|
|
5826
|
+
return refuse(response, who, 403, "Only an operator can change update checks.", "/settings#updates");
|
|
5827
|
+
const on = body.get("check") === "on";
|
|
5828
|
+
setUpdateChecks(options.configDir, on);
|
|
5829
|
+
return redirect(response, `/settings?said=${encodeURIComponent(on ? "Toolroll looks for a newer version once a day." : "Update checks are off.")}#updates`);
|
|
5830
|
+
}
|
|
5757
5831
|
if (url.pathname === '/code' || url.pathname.startsWith('/code/')) {
|
|
5758
5832
|
const wantsJson = request.headers.accept?.includes('application/json') === true;
|
|
5759
5833
|
const fail = (status, message, delivery = 'rejected', sessionId) => wantsJson
|
|
@@ -6458,6 +6532,52 @@ export function createDecisionServer(options) {
|
|
|
6458
6532
|
store.recordAction({ at: now.toISOString(), actor: who.name, repo: null, taskId: null, runId: null, action: before === null ? "sign-in with a provider turned on" : "sign-in with a provider changed", outcome: "changed", source: "policy", detail: ssoChangeWords(before, saved.settings) });
|
|
6459
6533
|
return found.ok ? back("said", `Saved. People can sign in with ${saved.settings.label}.`) : back("problem", `Saved, but ${saved.settings.label} didn't answer: ${found.said}`);
|
|
6460
6534
|
}
|
|
6535
|
+
// Settings → Updates: start the `toolroll update` job. A step-up; the job writes the ledger entries.
|
|
6536
|
+
if (url.pathname === "/settings/updates" || url.pathname === "/settings/updates/seen" || url.pathname === "/settings/updates/cancel") {
|
|
6537
|
+
const databaseFile = store.databaseFile();
|
|
6538
|
+
if (who.via !== "cookie" || !store.isInstanceOperator(who.name) || databaseFile === null)
|
|
6539
|
+
return refuse(response, who, 403, "An instance operator updates Toolroll.", "/settings");
|
|
6540
|
+
const stateDir = dirname(databaseFile);
|
|
6541
|
+
const back = (key, words) => redirect(response, `/settings/updates?${key}=${encodeURIComponent(words)}`);
|
|
6542
|
+
if (url.pathname === "/settings/updates/seen") {
|
|
6543
|
+
markWhatsNewSeen(stateDir);
|
|
6544
|
+
return redirect(response, "/settings/updates");
|
|
6545
|
+
}
|
|
6546
|
+
if (url.pathname === "/settings/updates/cancel")
|
|
6547
|
+
return back("said", requestRuntimeUpdateCancel(stateDir));
|
|
6548
|
+
if (!authenticateApprover(store, who.name, body.get("password") ?? "").ok)
|
|
6549
|
+
return back("problem", "Enter your Toolroll password to update.");
|
|
6550
|
+
const version = body.get("version") ?? "";
|
|
6551
|
+
const when = { now: "now", "when-idle": "when-idle", tonight: "at" }[body.get("when") ?? ""] ?? null;
|
|
6552
|
+
if (!/^\d+\.\d+\.\d+$/.test(version) || when === null)
|
|
6553
|
+
return back("problem", "Choose when to update.");
|
|
6554
|
+
const current = options.updates?.current ?? PACKAGE_VERSION;
|
|
6555
|
+
if (!newerThan(version, current))
|
|
6556
|
+
return back("problem", `Toolroll ${version} is not newer than ${current}. To go back, run toolroll update --rollback.`);
|
|
6557
|
+
const method = options.updates?.method ?? installMethod();
|
|
6558
|
+
if (method.kind === "npx" || method.kind === "source")
|
|
6559
|
+
return back("problem", method.kind === "npx" ? "npx runs the latest release each time; there is nothing to update." : "This runs from a source checkout. Update it with git.");
|
|
6560
|
+
const before = runtimeUpdateStatus(stateDir).journal;
|
|
6561
|
+
if (before !== null && !runtimeUpdateTerminal(before.phase))
|
|
6562
|
+
return back("problem", "An update is already under way.");
|
|
6563
|
+
if (when === "now") {
|
|
6564
|
+
const running = runningWorkWords(store.raw());
|
|
6565
|
+
if (running !== null)
|
|
6566
|
+
return back("problem", `Work is running: ${running}. Choose When idle to update once it finishes.`);
|
|
6567
|
+
}
|
|
6568
|
+
const prepared = prepareRuntimeUpdate({ stateDir, databaseFile, current: { version: current, dist: options.updates?.dist ?? currentRuntime(current).dist }, actor: who.name, version, when, at: "03:00" }, clock());
|
|
6569
|
+
if ("refused" in prepared)
|
|
6570
|
+
return back("problem", prepared.refused);
|
|
6571
|
+
try {
|
|
6572
|
+
await (options.updates?.launch ?? launchRuntimeUpdate)({ databaseFile, id: prepared.id });
|
|
6573
|
+
}
|
|
6574
|
+
catch (error) {
|
|
6575
|
+
abandonRuntimeUpdate(stateDir, prepared.id, `The update could not start: ${error.message}`, clock());
|
|
6576
|
+
return back("problem", `The update could not start: ${error.message}`);
|
|
6577
|
+
}
|
|
6578
|
+
// The job records its first step within moments; show it rather than the old page.
|
|
6579
|
+
return back("said", when === "at" ? `Update to ${version} scheduled for 03:00.` : `Updating to ${version}.`);
|
|
6580
|
+
}
|
|
6461
6581
|
// v105: change how long each kind of data is kept. A step-up; the ledger keeps before → after for each kind.
|
|
6462
6582
|
if (url.pathname === "/settings/retention") {
|
|
6463
6583
|
if (who.via !== "cookie" || !store.isInstanceOperator(who.name))
|
|
@@ -13917,10 +14037,10 @@ button.pick-file { min-height: 1.75rem; padding: 0 .55rem; font-size: .75rem; }
|
|
|
13917
14037
|
}
|
|
13918
14038
|
`;
|
|
13919
14039
|
/** Appearance: a three-way segmented switch, one tap per choice. */
|
|
13920
|
-
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}}`;
|
|
14040
|
+
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}`;
|
|
13921
14041
|
/** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
|
|
13922
14042
|
export const PAGE_CSS = STYLE + THEME_CONTROLS_CSS;
|
|
13923
|
-
const WORKSPACE_STYLE = styleAsset(STYLE + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_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}');
|
|
14043
|
+
const WORKSPACE_STYLE = styleAsset(STYLE + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_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}');
|
|
13924
14044
|
/**
|
|
13925
14045
|
* The board's liveness: fetch this page's own fragment on a timer and swap
|
|
13926
14046
|
* it in place — no flicker, no scroll reset, no long-lived stream to manage,
|
|
@@ -14327,7 +14447,11 @@ function shell(title, body, options = {}) {
|
|
|
14327
14447
|
? ""
|
|
14328
14448
|
: `<div class="banner"><span class="badge badge-running">mode</span>${escape(chrome.modeBanner.words)} \u00b7 <a href="/mode">the terms \u00b7 end it</a></div>`) +
|
|
14329
14449
|
(chrome.signIn ?? []).map(one => `<div class="banner sign-in-banner" data-sign-in="${escape(one.provider)}"><strong>${escape(one.title)}</strong> \u00b7 run <code>${escape(one.command)}</code> on this computer, then resume.${one.detail === "" ? "" : ` ${escape(one.detail)}`}` +
|
|
14330
|
-
`<form method="post" action="${escape(one.resumeHref)}" class="inline"><input type="hidden" name="csrf" value="${escape(chrome.csrf ?? "")}"><button type="submit">${escape(one.resumeLabel)}</button></form></div>`).join("")
|
|
14450
|
+
`<form method="post" action="${escape(one.resumeHref)}" class="inline"><input type="hidden" name="csrf" value="${escape(chrome.csrf ?? "")}"><button type="submit">${escape(one.resumeLabel)}</button></form></div>`).join("") +
|
|
14451
|
+
(chrome.update === undefined
|
|
14452
|
+
? ""
|
|
14453
|
+
: `<div class="banner update-banner" data-update="${escape(chrome.update.version)}">${escape(updateNoticeWords(chrome.update))} \u00b7 <a href="${escape(chrome.update.href)}">What's new</a>` +
|
|
14454
|
+
`<form method="post" action="${escape(chrome.update.dismissHref)}" class="inline"><input type="hidden" name="csrf" value="${escape(chrome.csrf ?? "")}"><input type="hidden" name="version" value="${escape(chrome.update.version)}"><button type="submit">Dismiss</button></form></div>`);
|
|
14331
14455
|
// The scope bar sits between the banners and the main/split body, so it
|
|
14332
14456
|
// can never disappear with a responsive pane (portfolio arc §1).
|
|
14333
14457
|
const content = chrome.listPane === undefined
|
|
@@ -14567,26 +14691,20 @@ function inboxPage(chrome, data) {
|
|
|
14567
14691
|
`<span class="meta">frees ${gap.unblocks.length} task${gap.unblocks.length > 1 ? "s" : ""}</span>` +
|
|
14568
14692
|
`<span class="right meta">how to fix \u2192</span></p>`)
|
|
14569
14693
|
.join("\n");
|
|
14570
|
-
// The first-run
|
|
14571
|
-
//
|
|
14572
|
-
// whole card retires permanently on the first successful run.
|
|
14694
|
+
// The first-run steps, until the first Ready result: each is done or
|
|
14695
|
+
// shows the one action that does it.
|
|
14573
14696
|
const wizard = data.wizard === null
|
|
14574
14697
|
? ""
|
|
14575
|
-
: `<div class="card">` +
|
|
14576
|
-
`<p><strong>
|
|
14577
|
-
`<p class="meta">Keep <span class="mono">toolroll up</span> running on this machine \u2014 it opens the app and reconnects every saved project's builder.</p>` +
|
|
14698
|
+
: `<div class="card" data-first-run>` +
|
|
14699
|
+
`<p><strong>Get to your first result</strong></p>` +
|
|
14578
14700
|
data.wizard
|
|
14579
|
-
.map(step => `<p class="row"><span class="mono">${step.done ? "\u2713" : "\u25cb"}</span> <strong>${escape(step.title)}</strong
|
|
14580
|
-
|
|
14701
|
+
.map(step => `<p class="row" data-step="${step.key}"><span class="mono" aria-hidden="true">${step.done ? "\u2713" : "\u25cb"}</span> <strong>${escape(step.title)}</strong>` +
|
|
14702
|
+
(step.action === null ? ` <span class="meta">done</span>`
|
|
14703
|
+
: step.action.kind === "link" ? ` <a href="${escape(step.action.href)}">${escape(step.action.label)}</a>`
|
|
14704
|
+
: ` <code>${escape(step.action.command)}</code>`) +
|
|
14705
|
+
`</p>`)
|
|
14581
14706
|
.join("\n") +
|
|
14582
|
-
|
|
14583
|
-
`<p class="meta">templates \u2014 edit, then approve; nothing a template files carries authority: ` +
|
|
14584
|
-
TEMPLATES.map(one => one.kind === "routine"
|
|
14585
|
-
? `<a href="/routines?template=${escape(one.name)}">${escape(one.name)}</a>`
|
|
14586
|
-
: one.kind === "task"
|
|
14587
|
-
? `<a href="/tasks?template=${escape(one.name)}">${escape(one.name)}</a>`
|
|
14588
|
-
: `<span title="a recipe \u2014 walk it in the terminal: toolroll template show ${escape(one.name)}">${escape(one.name)} (recipe)</span>`).join(" \u00b7 ") +
|
|
14589
|
-
`</p></div>`;
|
|
14707
|
+
`</div>`;
|
|
14590
14708
|
const noWorker = data.worker.answering > 0
|
|
14591
14709
|
? ""
|
|
14592
14710
|
: `<div class="card builder-notice" data-builder-status="${data.worker.registered === 0 ? "not-connected" : "disconnected"}">` +
|
|
@@ -21345,7 +21463,7 @@ function telegramDeliveryWords(store, bot) {
|
|
|
21345
21463
|
return `Telegram pushes new messages to ${state.url}${state.problem === null ? "." : `, but ${state.problem.charAt(0).toLowerCase()}${state.problem.slice(1)}.`}`;
|
|
21346
21464
|
return `Toolroll asks Telegram for new messages every few seconds${state?.problem ? ` (${state.problem})` : ""}.`;
|
|
21347
21465
|
}
|
|
21348
|
-
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) {
|
|
21466
|
+
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) {
|
|
21349
21467
|
const permissionCard = permissionDefault === null
|
|
21350
21468
|
? ""
|
|
21351
21469
|
: [
|
|
@@ -21541,6 +21659,8 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
21541
21659
|
telegram: { state: hasEnv ? "from the environment" : existing === null ? "not set" : "saved", current, delivery: telegramDelivery },
|
|
21542
21660
|
email,
|
|
21543
21661
|
workers,
|
|
21662
|
+
updates,
|
|
21663
|
+
firstResult,
|
|
21544
21664
|
};
|
|
21545
21665
|
return screen("Settings", [
|
|
21546
21666
|
"<h1>Settings</h1>",
|
|
@@ -21550,6 +21670,8 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
21550
21670
|
qualityCard,
|
|
21551
21671
|
keysCard,
|
|
21552
21672
|
workersCard(workers),
|
|
21673
|
+
updatesCard(updates, csrf),
|
|
21674
|
+
firstResult === null ? "" : `<p class="meta" data-first-result>${escape(firstResult)}</p>`,
|
|
21553
21675
|
pushCard === "" && messagingCard === "" && digestCard === "" ? "" : `<h2>Notifications</h2>`,
|
|
21554
21676
|
messagingCard,
|
|
21555
21677
|
pushCard,
|
|
@@ -21604,9 +21726,42 @@ function workersCard(workers) {
|
|
|
21604
21726
|
`<p class="meta">To change how many a worker runs at once: <code>toolroll runner capacity <name> <n></code></p>`) +
|
|
21605
21727
|
`</section>`;
|
|
21606
21728
|
}
|
|
21729
|
+
/** Settings → Updates, for browsers without the app script. */
|
|
21730
|
+
function updatesCard(updates, csrf) {
|
|
21731
|
+
if (updates === null)
|
|
21732
|
+
return "";
|
|
21733
|
+
const latest = updates.latest;
|
|
21734
|
+
return `<section id="updates" aria-labelledby="updates-title"><h2 id="updates-title">Updates</h2>` +
|
|
21735
|
+
`<p class="row">This version <span class="mono">${escape(updates.current)}</span> \u00b7 ${!updates.check.on ? "Update checks are off" : latest === null ? "Not checked yet" : latest.newer ? `Latest <span class="mono">${escape(latest.version)}</span>` : "Up to date"}</p>` +
|
|
21736
|
+
(latest !== null && latest.newer
|
|
21737
|
+
? `<p>Update with <code>${escape(updates.updateCommand)}</code> \u00b7 <a href="${escape(latest.url)}">Release notes</a></p>` +
|
|
21738
|
+
(latest.notes === "" ? "" : `<details class="settings-more"><summary>What's new in ${escape(latest.version)}</summary><pre class="update-notes">${escape(latest.notes)}</pre></details>`)
|
|
21739
|
+
: "") +
|
|
21740
|
+
(updates.check.canManage && !updates.check.byEnv
|
|
21741
|
+
? `<form method="post" action="/settings/updates/checks" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="check" value="${updates.check.on ? "off" : "on"}"><button type="submit">${updates.check.on ? "Turn off daily check" : "Turn on daily check"}</button></form>`
|
|
21742
|
+
: updates.check.byEnv ? `<p class="meta">Off by TOOLROLL_NO_UPDATE_CHECK.</p>` : "") +
|
|
21743
|
+
(updates.workers.length === 0 ? "" : `<ul>${updates.workers.map(one => `<li data-worker-version="${escape(one.name)}">${escape(one.name)} <span class="mono">${escape(one.version ?? "unknown")}</span>${one.older ? " \u00b7 older" : ""}</li>`).join("")}</ul>`) +
|
|
21744
|
+
`</section>`;
|
|
21745
|
+
}
|
|
21607
21746
|
/** Choices save the moment they change; without the script the Save
|
|
21608
21747
|
* button stays and the form works the same. */
|
|
21609
21748
|
const SETTINGS_AUTOSAVE_SCRIPT = `(function(){document.querySelectorAll('form[data-autosave]').forEach(function(form){form.classList.add('js-autosave');form.addEventListener('change',function(ev){var t=ev.target;if(t&&(t.type==='radio'||t.tagName==='SELECT')){if(form.requestSubmit)form.requestSubmit();else form.submit();}});});})();`;
|
|
21749
|
+
/** The newest release, remembered for a while so the page stays quick. */
|
|
21750
|
+
let latestSeen = null;
|
|
21751
|
+
async function latestReleaseFor(latest) {
|
|
21752
|
+
if (latest === undefined && latestSeen !== null && Date.now() - latestSeen.at < ("problem" in latestSeen.value ? 60_000 : 900_000))
|
|
21753
|
+
return latestSeen.value;
|
|
21754
|
+
let value;
|
|
21755
|
+
try {
|
|
21756
|
+
value = { version: (await (latest ?? latestVersionNow)()).version };
|
|
21757
|
+
}
|
|
21758
|
+
catch (error) {
|
|
21759
|
+
value = { problem: error.message };
|
|
21760
|
+
}
|
|
21761
|
+
if (latest === undefined)
|
|
21762
|
+
latestSeen = { at: Date.now(), value };
|
|
21763
|
+
return value;
|
|
21764
|
+
}
|
|
21610
21765
|
/** Settings destinations as a scannable grid: an icon and a name each. */
|
|
21611
21766
|
function settingsTiles() {
|
|
21612
21767
|
const tiles = SETTINGS_TILE_ICONS;
|
|
@@ -21629,6 +21784,7 @@ const SETTINGS_TILE_ICONS = [
|
|
|
21629
21784
|
["/settings/approval", "Approval rules", `<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/><path d="m9 12 2 2 4-4"/>`],
|
|
21630
21785
|
["/settings/monitoring", "Monitoring", `<path d="M22 12h-4l-3 9L9 3l-3 9H2"/>`],
|
|
21631
21786
|
["/settings/retention", "Retention", `<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>`],
|
|
21787
|
+
["/settings/updates", "Updates", `<path d="M21 12a9 9 0 1 1-2.64-6.36"/><path d="M21 3v6h-6"/>`],
|
|
21632
21788
|
["/settings/backups", "Backups", `<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 1.7 4 3 9 3s9-1.3 9-3V5"/><path d="M3 12c0 1.7 4 3 9 3s9-1.3 9-3"/>`],
|
|
21633
21789
|
["/settings/data", "Data", `<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><path d="m7 10 5 5 5-5"/><path d="M12 15V3"/>`],
|
|
21634
21790
|
];
|
package/dist/skills.d.ts
CHANGED
|
@@ -68,6 +68,8 @@ export declare function applyInstall(repo: string, writeContext: boolean): Insta
|
|
|
68
68
|
*/
|
|
69
69
|
export declare const CLAUDE_CODE_MANAGED_MARK = "<!-- standing-orders:claude-code-skill:v1 -->";
|
|
70
70
|
export declare const CLAUDE_CODE_GUIDES: readonly ["console", "operating", "runner"];
|
|
71
|
+
/** The thin user-level skill `toolroll onboard` writes (agent-onboard.ts). */
|
|
72
|
+
export declare const OPERATOR_SKILL_MARK = "<!-- toolroll:operator-skill:v1";
|
|
71
73
|
export type ClaudeCodeSkillFile = {
|
|
72
74
|
name: string;
|
|
73
75
|
path: string;
|
package/dist/skills.js
CHANGED
|
@@ -162,6 +162,8 @@ export function applyInstall(repo, writeContext) {
|
|
|
162
162
|
*/
|
|
163
163
|
export const CLAUDE_CODE_MANAGED_MARK = "<!-- standing-orders:claude-code-skill:v1 -->";
|
|
164
164
|
export const CLAUDE_CODE_GUIDES = ["console", "operating", "runner"];
|
|
165
|
+
/** The thin user-level skill `toolroll onboard` writes (agent-onboard.ts). */
|
|
166
|
+
export const OPERATOR_SKILL_MARK = "<!-- toolroll:operator-skill:v1";
|
|
165
167
|
export function defaultClaudeCodeSkillDir(home = homedir()) {
|
|
166
168
|
return join(home, ".claude", "skills", "toolroll");
|
|
167
169
|
}
|
|
@@ -232,7 +234,7 @@ function claudeCodeFiles(directory) {
|
|
|
232
234
|
}
|
|
233
235
|
function isClaudeCodeManaged(name, content) {
|
|
234
236
|
if (name === "SKILL.md") {
|
|
235
|
-
return ownHeader(content) && content.includes(`\n${CLAUDE_CODE_MANAGED_MARK}\n`);
|
|
237
|
+
return ownHeader(content) && (content.includes(`\n${CLAUDE_CODE_MANAGED_MARK}\n`) || content.includes(`\n${OPERATOR_SKILL_MARK} `));
|
|
236
238
|
}
|
|
237
239
|
return content.startsWith(`${CLAUDE_CODE_MANAGED_MARK}\n\n`);
|
|
238
240
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -4344,9 +4344,9 @@ export declare class Store {
|
|
|
4344
4344
|
* checklist (adoption review, finding 14).
|
|
4345
4345
|
*/
|
|
4346
4346
|
firstSuccessAt(now: Date): string | null;
|
|
4347
|
-
/**
|
|
4348
|
-
*
|
|
4349
|
-
|
|
4347
|
+
/** When this installation began: its first account. Accounts are never
|
|
4348
|
+
* deleted (revocation is a stamp), so this never moves. */
|
|
4349
|
+
installationStartedAt(): string | null;
|
|
4350
4350
|
/** The jump palette's food: open (queued/running/failed) tasks, admission
|
|
4351
4351
|
* inside the query before its LIMIT, newest first. */
|
|
4352
4352
|
paletteTasks(repo: string | null, limit: number, admitted?: string[] | null, includeUnplaced?: boolean): {
|
package/dist/store.js
CHANGED
|
@@ -14410,10 +14410,11 @@ export class Store {
|
|
|
14410
14410
|
this.recordInstallationFact("first-success-at", when, now);
|
|
14411
14411
|
return when;
|
|
14412
14412
|
}
|
|
14413
|
-
/**
|
|
14414
|
-
*
|
|
14415
|
-
|
|
14416
|
-
|
|
14413
|
+
/** When this installation began: its first account. Accounts are never
|
|
14414
|
+
* deleted (revocation is a stamp), so this never moves. */
|
|
14415
|
+
installationStartedAt() {
|
|
14416
|
+
const row = this.db.prepare("SELECT MIN(added_at) AS at FROM approver").get();
|
|
14417
|
+
return row === undefined || row["at"] == null ? null : String(row["at"]);
|
|
14417
14418
|
}
|
|
14418
14419
|
/** The jump palette's food: open (queued/running/failed) tasks, admission
|
|
14419
14420
|
* inside the query before its LIMIT, newest first. */
|
|
@@ -15936,6 +15937,10 @@ export class Store {
|
|
|
15936
15937
|
// stop after an orphan or held supervisor finishes shutdown.
|
|
15937
15938
|
this.recordRunProcessExits(id, result.now);
|
|
15938
15939
|
this.settleRunStop(id, result.stopSettlement ?? "finished", result.now);
|
|
15940
|
+
// The first Ready result is an installation fact the moment it lands,
|
|
15941
|
+
// not only when something later looks for it.
|
|
15942
|
+
if (result.outcome === "built" || result.outcome === "no-change")
|
|
15943
|
+
this.firstSuccessAt(result.now);
|
|
15939
15944
|
// The park rate is *measured* — parked over concluded builder attempts —
|
|
15940
15945
|
// and maintained where attempts conclude, because the attention budget's
|
|
15941
15946
|
// gate reads it inside a claim transaction and must never trust a number
|