toolroll 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/agent-onboard.d.ts +21 -1
- package/dist/agent-onboard.js +50 -1
- package/dist/backup-ui.js +4 -2
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +31 -31
- package/dist/browser-workspace.d.ts +12 -2
- package/dist/chat-actions.d.ts +5 -0
- package/dist/chat-actions.js +43 -3
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/desktop-update.d.ts +1 -1
- package/dist/flow-engine.js +13 -1
- package/dist/flow-pull-request.d.ts +11 -0
- package/dist/flow-pull-request.js +123 -0
- package/dist/flow-starters-ui.d.ts +20 -0
- package/dist/flow-starters-ui.js +35 -0
- package/dist/flow-starters.d.ts +55 -0
- package/dist/flow-starters.js +104 -0
- package/dist/flow-steps.js +14 -0
- package/dist/flows-ui.d.ts +1 -1
- package/dist/flows-ui.js +4 -3
- package/dist/flows.d.ts +30 -6
- package/dist/flows.js +110 -6
- package/dist/integrations-ui.js +9 -4
- package/dist/lead-context.d.ts +1 -1
- package/dist/lead-context.js +1 -1
- package/dist/mate-contract.js +1 -1
- package/dist/mate-tools.js +18 -5
- package/dist/models-ui.js +3 -2
- package/dist/monitoring-ui.js +2 -1
- package/dist/operate.js +18 -0
- package/dist/policy-ui.js +2 -1
- package/dist/pull-request-flow.d.ts +32 -0
- package/dist/pull-request-flow.js +43 -8
- package/dist/pull-request-ui.js +2 -2
- package/dist/serve.js +108 -43
- package/dist/skills-ui.js +2 -1
- package/dist/store.js +2 -0
- package/dist/surface.js +1 -1
- package/dist/when-html.d.ts +8 -0
- package/dist/when-html.js +36 -0
- package/package.json +1 -1
package/dist/serve.js
CHANGED
|
@@ -25,6 +25,8 @@ import { grantTool, revokeTool, rulesFromForm, setToolRules } from "./teammate-t
|
|
|
25
25
|
import { addKitGithubTrigger, addKitSample, kitInstalled, kitOf, setUpKit } from "./kits.js";
|
|
26
26
|
import { CONNECT_CALLBACK, connectionsOf, finishConnect, oneClickOf, startConnect } from "./mcp-connect.js";
|
|
27
27
|
import { KITS_CSS, kitPageHtml, kitsGalleryHtml } from "./kits-ui.js";
|
|
28
|
+
import { STARTERS_CSS, startersHtml } from "./flow-starters-ui.js";
|
|
29
|
+
import { starterForWork, starterOf, startersFor, switchOnStarter } from "./flow-starters.js";
|
|
28
30
|
import { createTeammateFrom, labelOf, nameOf, saveSoul, setTeammateState, teammateSettings, sendTeammateSummaries } from "./teammate-admin.js";
|
|
29
31
|
import { editMemory, forgetMemory, tellTeammate } from "./teammate-memory.js";
|
|
30
32
|
import { addRoutine, removeRoutine, runRoutine } from "./teammate-desk.js";
|
|
@@ -246,6 +248,7 @@ import { cachedRelease, isNewer, runnerVersions, setUpdateChecks, updateChecksOf
|
|
|
246
248
|
import { installMethod } from "./install-method.js";
|
|
247
249
|
import { PACKAGE_VERSION } from "./version.js";
|
|
248
250
|
import { updateNoticeWords } from "./update-notice.js";
|
|
251
|
+
import { whenHtml } from "./when-html.js";
|
|
249
252
|
const SESSION_COOKIE = "standing-orders_session";
|
|
250
253
|
const BODY_CAP = 16 * 1024;
|
|
251
254
|
// URL encoding can triple UTF-8 bytes. Admit the existing bounded task
|
|
@@ -1582,8 +1585,8 @@ export function createDecisionServer(options) {
|
|
|
1582
1585
|
refuse(response, who, 404, 'No such action in your projects.', '/projects');
|
|
1583
1586
|
return false;
|
|
1584
1587
|
}
|
|
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"]);
|
|
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"]);
|
|
1588
|
+
const read = new Set(["/settings/flows", "/settings/skills", "/settings/knowledge", "/settings", "/settings/learning", "/recipes", "/recipes/run", "/recipes/start", "/recipes/new", "/recipes/edit", "/recipes/from-task", "/recipes/preview", "/recipes/export", "/", "/inbox", "/work", "/projects", "/people", "/ledger", "/ledger/export", "/next", "/board", "/tasks", "/tasks/new", "/runs", "/review", "/done", "/routines", "/menu"]);
|
|
1589
|
+
const write = new Set(["/settings/flows/on", "/settings/skills/import", "/settings/skills/change", "/settings/skills/revise", "/settings/knowledge/change", "/settings/knowledge/refresh", "/settings/learning/change", "/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/save", "/recipes/launch", "/projects/select", "/tasks/add", "/routines/add"]);
|
|
1587
1590
|
const task = matchTaskPath(path, request.method === "GET" ? "(/evidence)?$" : "/(hold|unhold|requeue|cancel|scope|approve|plan|plan-edit|next|reopen|steer|accept-proof|accept-revision|reject-revision|route|retry-review|complete|merge|stop|resume-arm|resume)$");
|
|
1588
1591
|
const resource = request.method === "GET"
|
|
1589
1592
|
? /^\/(?:r|d)\/[0-9]{1,15}(?:\/evidence\/[0-9]{1,15})?$/.test(path) || /^\/routines\/[0-9]{1,15}$/.test(path) || path === "/flows" || /^\/flows\/[0-9]{1,15}(\/insights|\/runs\/[0-9]{1,15}\/[0-9]{1,15})?$/.test(path)
|
|
@@ -3639,6 +3642,20 @@ export function createDecisionServer(options) {
|
|
|
3639
3642
|
sessions: store.webSessions(everyone ? null : who.name).map(one => ({ ...one, here: one.idHash === here })), tokens: store.apiTokens(everyone ? null : who.name) };
|
|
3640
3643
|
return sendScreen(response, 200, screen("Sessions & tokens", `<p><a href="/settings">Settings</a></p><h1>Sessions & tokens</h1>${credentialsHtml(view, who.session.csrf, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "settings") }));
|
|
3641
3644
|
}
|
|
3645
|
+
// Settings → Flows: the starter flows, each switched on with one yes. ?starter= marks the one a task's
|
|
3646
|
+
// "Do this every time…" asked about.
|
|
3647
|
+
if (url.pathname === "/settings/flows") {
|
|
3648
|
+
const projects = [...new Set([...(admissionList() ?? []), ...managedRepos(), ...store.knownRepos()])].filter(visible);
|
|
3649
|
+
const chosen = url.searchParams.get("repo") ?? project ?? projects[0] ?? "";
|
|
3650
|
+
if (chosen && !projects.includes(chosen))
|
|
3651
|
+
return refuse(response, who, 403, "That project is outside your access.", "/settings");
|
|
3652
|
+
const content = chosen === "" ? "<p>Add a project to switch on starter flows.</p>" : startersHtml({
|
|
3653
|
+
repo: chosen, projects: projects.map(path => ({ path, name: projectName(path) })), starters: startersFor(store, chosen), csrf: who.via === "cookie" ? who.session.csrf : "",
|
|
3654
|
+
canSwitch: who.via === "cookie" && who.role === "approver" && !store.isDemo(), suggested: starterOf(url.searchParams.get("starter") ?? "")?.id ?? null,
|
|
3655
|
+
said: url.searchParams.get("said"), problem: url.searchParams.get("problem")
|
|
3656
|
+
});
|
|
3657
|
+
return sendScreen(response, 200, screen("Flows", `<p><a href="/settings">Settings</a></p><h1>Flows</h1>${content}`, { chrome: chromeFor(chosen || project, "settings") }));
|
|
3658
|
+
}
|
|
3642
3659
|
// Settings → Integrations: which integrations work. The list is the last checks; a render never waits on one.
|
|
3643
3660
|
if (url.pathname === "/settings/integrations") {
|
|
3644
3661
|
if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
|
|
@@ -4890,7 +4907,7 @@ export function createDecisionServer(options) {
|
|
|
4890
4907
|
.map(run => `<a class="item${run.id === currentId ? " current" : ""}" href="/r/${run.id}">` +
|
|
4891
4908
|
`<span class="t">#${run.id} \u00b7 ${escape(run.taskId)}</span>` +
|
|
4892
4909
|
`<span class="m">${runOutcomeBadge(run, live.has(run.id))}` +
|
|
4893
|
-
`<span class="mono">${
|
|
4910
|
+
`<span class="mono">${whenTime(run.startedAt)}</span></span></a>`)
|
|
4894
4911
|
.join("\n");
|
|
4895
4912
|
return `<h2>builds</h2>\n${items === "" ? `<p class="meta">none yet</p>` : items}`;
|
|
4896
4913
|
}
|
|
@@ -6274,7 +6291,12 @@ export function createDecisionServer(options) {
|
|
|
6274
6291
|
return redirect(response, `/flows?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
6275
6292
|
const template = FLOW_TEMPLATES.find(one => one.id === body.get("template")) ?? FLOW_TEMPLATES[0];
|
|
6276
6293
|
const name = (body.get("name") ?? "").replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 80) || template.label;
|
|
6277
|
-
|
|
6294
|
+
const made = store.createFlow({ repo, name, definitionJson: JSON.stringify(template.definition), by: who.name }, now);
|
|
6295
|
+
// A template that starts from something (Issues to PRs: labelled issues) comes with its trigger.
|
|
6296
|
+
const trigger = template.trigger === undefined ? null : addFlowTriggerTo(store, store.getFlow(made), template.trigger, who.name, now, options.configDir ?? null);
|
|
6297
|
+
if (trigger !== null && !trigger.ok)
|
|
6298
|
+
return redirect(response, `/flows?problem=${encodeURIComponent(`${name} was made, but its trigger wasn't: ${trigger.message} Add one on its Triggers panel.`)}`);
|
|
6299
|
+
return redirect(response, `/flows/${made}`);
|
|
6278
6300
|
}
|
|
6279
6301
|
const flow = store.getFlow(Number((flowPost ?? triggerPost)[1]));
|
|
6280
6302
|
if (flow === null || flow.state !== "active" || !visible(flow.repo))
|
|
@@ -6831,6 +6853,21 @@ export function createDecisionServer(options) {
|
|
|
6831
6853
|
"cache-control": "no-store", "x-content-type-options": "nosniff" });
|
|
6832
6854
|
return void response.end(zip);
|
|
6833
6855
|
}
|
|
6856
|
+
// Settings → Flows → Switch on: a starter flow's zones and trigger, made together.
|
|
6857
|
+
if (url.pathname === "/settings/flows/on") {
|
|
6858
|
+
const repo = body.get("repo") ?? "";
|
|
6859
|
+
const back = (key, words) => redirect(response, `/settings/flows?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
6860
|
+
const known = [...new Set([...(admissionList() ?? []), ...managedRepos(), ...store.knownRepos()])];
|
|
6861
|
+
if (who.via !== "cookie" || who.role !== "approver" || store.isDemo())
|
|
6862
|
+
return refuse(response, who, 403, "An approver switches starter flows on.", "/settings/flows");
|
|
6863
|
+
if (!known.includes(repo) || !visible(repo))
|
|
6864
|
+
return refuse(response, who, 404, "No such project.", "/settings/flows");
|
|
6865
|
+
const starter = starterOf(body.get("starter") ?? "");
|
|
6866
|
+
if (starter === null)
|
|
6867
|
+
return back("problem", "Choose a starter flow.");
|
|
6868
|
+
const switched = switchOnStarter(store, starter, repo, who.name, clock(), options.configDir ?? null);
|
|
6869
|
+
return switched.ok ? back("said", switched.said) : back("problem", switched.said);
|
|
6870
|
+
}
|
|
6834
6871
|
// Settings → Integrations → Send test: one harmless read-only check of one integration, now.
|
|
6835
6872
|
if (url.pathname === "/settings/integrations/test") {
|
|
6836
6873
|
if (who.via !== "cookie" || who.role !== "approver" || restricted() || !options.configDir)
|
|
@@ -9394,7 +9431,7 @@ export function createDecisionServer(options) {
|
|
|
9394
9431
|
`<p class="meta">worker</p><p class="recap" style="margin-top:0">${escape(terms.runner)} (this machine)</p>` +
|
|
9395
9432
|
`<p class="meta">spending</p><p class="recap" style="margin-top:0">up to about $${(terms.budgetMicrousd / 1_000_000).toFixed(2)} — the agent stops as soon as its total crosses this; the final step may run a little past it</p>` +
|
|
9396
9433
|
`<p class="meta">conversation</p><p class="recap" style="margin-top:0">at most ${terms.maxSessionTurns} messages to the agent, this whole session; each may work up to ${Math.round(terms.turnTimeoutSeconds / 60)} minutes — one that runs past that ends the whole session. If it needs a repair, the repair uses the same session, model, and clock.</p>` +
|
|
9397
|
-
`<p class="meta">while you watch — a signed term</p><p class="recap" style="margin-top:0">your console being open is what keeps it running — any page of it, this one included; close the console and the session winds down within a minute. Everything ends by ${
|
|
9434
|
+
`<p class="meta">while you watch — a signed term</p><p class="recap" style="margin-top:0">your console being open is what keeps it running — any page of it, this one included; close the console and the session winds down within a minute. Everything ends by ${whenTime(terms.absoluteExpiry)} regardless. One attempt; it never converts into unattended work.</p>` +
|
|
9398
9435
|
(quick
|
|
9399
9436
|
? `<p class="meta">your signed mode covers this mint — no password; the mode is re-proved as you confirm, and the session is stamped with its signature</p>`
|
|
9400
9437
|
: `<label>your password, typed again — a signed-in session alone cannot authorize work<input type="password" name="token" autocomplete="current-password"></label>`) +
|
|
@@ -10977,6 +11014,10 @@ function consoleSpend(summary) {
|
|
|
10977
11014
|
function when(iso) {
|
|
10978
11015
|
return iso === null ? "" : iso.slice(0, 16).replace("T", " ");
|
|
10979
11016
|
}
|
|
11017
|
+
/** The same minute as a `<time>`: the full stamp on a desk, "16:39" / "Yesterday 16:39" / "Sep 28" on a phone. */
|
|
11018
|
+
function whenTime(iso) {
|
|
11019
|
+
return iso === null ? "" : whenHtml(iso, when(iso));
|
|
11020
|
+
}
|
|
10980
11021
|
/** Overdue is derived at render — display never writes. */
|
|
10981
11022
|
function isOverdue(decision, now) {
|
|
10982
11023
|
if (decision.state === "expired")
|
|
@@ -11464,7 +11505,7 @@ function agentsAvailabilityHtml(projection) {
|
|
|
11464
11505
|
const items = [...seen.values()].map(leg => {
|
|
11465
11506
|
const state = leg.readiness === "ready" ? "ready" : leg.readiness === "unavailable" ? "unavailable" : "not yet checked";
|
|
11466
11507
|
const detail = leg.readiness === "unavailable" && leg.readinessReason !== null ? ` (${escape(leg.readinessReason)})` : "";
|
|
11467
|
-
return `<li class="agents-availability-${escape(leg.readiness)}"><span class="mono">${escape(leg.provider)}</span> ${state}${detail}${leg.readinessRunner === null ? "" : ` <span class="meta">— ${escape(leg.readinessRunner)}${leg.observedAt === null ? "" : `, ${
|
|
11508
|
+
return `<li class="agents-availability-${escape(leg.readiness)}"><span class="mono">${escape(leg.provider)}</span> ${state}${detail}${leg.readinessRunner === null ? "" : ` <span class="meta">— ${escape(leg.readinessRunner)}${leg.observedAt === null ? "" : `, ${whenTime(leg.observedAt)}`}</span>`}</li>`;
|
|
11468
11509
|
});
|
|
11469
11510
|
return `<p class="meta agents-availability-label">Availability right now</p><ul class="agents-availability">${items.join("")}</ul>`;
|
|
11470
11511
|
}
|
|
@@ -11554,7 +11595,7 @@ function agentsCardHtml(taskId, view, csrf, canEdit) {
|
|
|
11554
11595
|
const overrides = view.overrides.length === 0
|
|
11555
11596
|
? ""
|
|
11556
11597
|
: `<ul class="agents-overrides">${view.overrides
|
|
11557
|
-
.map(one => `<li><span>${escape(ROLE_NOUN[one.phase])} → <span class="mono">${escape(one.provider)} · ${escape(one.model)}</span> <span class="meta">by ${escape(one.by)} ${
|
|
11598
|
+
.map(one => `<li><span>${escape(ROLE_NOUN[one.phase])} → <span class="mono">${escape(one.provider)} · ${escape(one.model)}</span> <span class="meta">by ${escape(one.by)} ${whenTime(one.at)}</span></span>` +
|
|
11558
11599
|
(canEdit && view.editable ? `<form method="post" action="${taskHref(taskId)}/route" class="agents-clear">${hidden}<input type="hidden" name="clear-phase" value="${escape(one.phase)}"><button type="submit" class="secondary" aria-label="clear the ${escape(ROLE_NOUN[one.phase].toLowerCase())} choice">clear</button></form>` : "") +
|
|
11559
11600
|
`</li>`)
|
|
11560
11601
|
.join("")}</ul>`;
|
|
@@ -11727,6 +11768,9 @@ ${THEME_MAPPING}
|
|
|
11727
11768
|
--so-ease-out: cubic-bezier(.23, 1, .32, 1);
|
|
11728
11769
|
--font-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
11729
11770
|
--font-mono: "Geist Mono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
11771
|
+
/* Phone spacing steps (760px and narrower): side gutter, gap under the
|
|
11772
|
+
header, gap between page blocks, between rows, and inside a card. */
|
|
11773
|
+
--so-phone-gutter: 14px; --so-phone-gap: 12px; --so-phone-block: 14px; --so-phone-row: 8px; --so-phone-card: 12px;
|
|
11730
11774
|
}
|
|
11731
11775
|
@media (prefers-color-scheme: dark) {
|
|
11732
11776
|
:root:not([data-theme="light"]) {
|
|
@@ -14297,12 +14341,27 @@ button.pick-file { min-height: 1.75rem; padding: 0 .55rem; font-size: .75rem; }
|
|
|
14297
14341
|
}
|
|
14298
14342
|
.sticky-actions button { margin: 0; }
|
|
14299
14343
|
}
|
|
14344
|
+
|
|
14345
|
+
/* A time a person reads (when-html.ts): the full stamp on a desk, the short
|
|
14346
|
+
one on a phone; the full stamp stays in the title. */
|
|
14347
|
+
.so-when-short { display: none; }
|
|
14348
|
+
@media (max-width: 760px) {
|
|
14349
|
+
.so-when-full { display: none; }
|
|
14350
|
+
.so-when-short { display: inline; }
|
|
14351
|
+
/* Server pages outside the workspace keep the same phone rhythm. */
|
|
14352
|
+
main { padding: var(--so-phone-gap) var(--so-phone-gutter) 3rem; }
|
|
14353
|
+
h2 { margin: 1.25rem 0 .375rem; }
|
|
14354
|
+
.card { padding: var(--so-phone-card); margin: var(--so-phone-row) 0; }
|
|
14355
|
+
.problem { padding: .5rem .625rem; margin: var(--so-phone-row) 0; }
|
|
14356
|
+
.meta, .hint { line-height: 1.35; }
|
|
14357
|
+
details { margin: var(--so-phone-row) 0; }
|
|
14358
|
+
}
|
|
14300
14359
|
`;
|
|
14301
14360
|
/** Appearance: a three-way segmented switch, one tap per choice. */
|
|
14302
14361
|
const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem;margin:0 0 2rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed="true"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}`;
|
|
14303
14362
|
/** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
|
|
14304
14363
|
export const PAGE_CSS = STYLE + THEME_CONTROLS_CSS;
|
|
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}');
|
|
14364
|
+
const WORKSPACE_STYLE = styleAsset(STYLE + APPROVAL_RULES_CSS + SPEND_CSS + RETENTION_CSS + STORAGE_CSS + UPDATES_CSS + LIMITS_CSS + MONITORING_CSS + INTEGRATIONS_CSS + BACKUP_CSS + EXPORT_CSS + PROJECT_DELETE_CSS + POLICY_CSS + EVIDENCE_PACK_CSS + THEME_CONTROLS_CSS + CODING_CSS + CODING_SHIPPING_CSS + RECIPE_CSS + SKILLS_CSS + TOOLS_CSS + FLOWS_CSS + TEAMMATE_CSS + KITS_CSS + STARTERS_CSS + SSO_CSS + CREDENTIALS_CSS + KNOWLEDGE_CSS + MODELS_CSS + CHAT_POLISH_CSS + TRANSITIONS_CSS + WORKSPACE_MOTION_CSS + ASSIGNMENT_CSS + LEAD_CONTEXT_CSS + PULL_REQUEST_SETTINGS_CSS + '.learning{min-width:0;overflow-wrap:anywhere}.learning .card{min-width:0}.learning code,.learning blockquote,.learning pre{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word}.learning button,.learning summary,.learning .button-link{min-height:44px}.learning button{white-space:nowrap}.learning summary{padding:12px 0;cursor:pointer}.learning form{margin:12px 0}.learning select{max-width:100%}.learning blockquote{margin:8px 0}.learning ul{padding-left:20px}');
|
|
14306
14365
|
/**
|
|
14307
14366
|
* The board's liveness: fetch this page's own fragment on a timer and swap
|
|
14308
14367
|
* it in place — no flicker, no scroll reset, no long-lived stream to manage,
|
|
@@ -14972,7 +15031,7 @@ function inboxPage(chrome, data) {
|
|
|
14972
15031
|
: `<div class="card builder-notice" data-builder-status="${data.worker.registered === 0 ? "not-connected" : "disconnected"}">` +
|
|
14973
15032
|
(data.worker.registered === 0
|
|
14974
15033
|
? `<strong>No builder is connected yet.</strong> Toolroll is open, but no machine is connected to do project work. On the machine where the project lives, open that folder and run <span class="mono">toolroll up</span>. Keep Toolroll running; approved work starts automatically.`
|
|
14975
|
-
: `<strong>Builder disconnected.</strong> ${data.worker.registered} builder${data.worker.registered === 1 ? " is" : "s are"} configured, last checked in ${data.worker.lastHeard === null ? "never" :
|
|
15034
|
+
: `<strong>Builder disconnected.</strong> ${data.worker.registered} builder${data.worker.registered === 1 ? " is" : "s are"} configured, last checked in ${data.worker.lastHeard === null ? "never" : whenTime(data.worker.lastHeard)}. Reopen Toolroll on that machine. Queued work starts automatically when a builder reconnects.`) +
|
|
14976
15035
|
`</div>`;
|
|
14977
15036
|
return screen("inbox", [
|
|
14978
15037
|
`<h1>Inbox</h1>`,
|
|
@@ -15040,8 +15099,8 @@ function systemPage(chrome, data) {
|
|
|
15040
15099
|
? ""
|
|
15041
15100
|
: `<div class="stat-card"><span class="k"><span class="dot ${data.episode.endedAt === null ? "dot-ok pulse" : "dot-off"}"></span>Toolroll</span>` +
|
|
15042
15101
|
`<span class="v">${data.episode.endedAt === null
|
|
15043
|
-
? `running since ${
|
|
15044
|
-
: `last run: ${data.episode.built} built, ${data.episode.broke} broke \u00b7 ended ${
|
|
15102
|
+
? `running since ${whenTime(data.episode.startedAt)}`
|
|
15103
|
+
: `last run: ${data.episode.built} built, ${data.episode.broke} broke \u00b7 ended ${whenTime(data.episode.endedAt)}`}</span></div>`;
|
|
15045
15104
|
const cards = [...runnerCards, watchCard, ...worktreeCards].filter(one => one !== "");
|
|
15046
15105
|
const PHASE_SAID = {
|
|
15047
15106
|
plan: "planning sessions ask questions and draft the plan you approve",
|
|
@@ -15311,7 +15370,7 @@ function donePage(chrome, rows, ciRed) {
|
|
|
15311
15370
|
return (`<div class="card"><p><a href="${taskHref(row.taskId)}"><strong>${escape(row.title)}</strong></a>` +
|
|
15312
15371
|
`${row.outcome === "no-change" ? ` <span class="badge">no change needed</span>` : ""}${pr}${needsVerification}${criterionMatrixSummary(row.proofMatrix)}</p>` +
|
|
15313
15372
|
`${row.handoff === null ? "" : `<p class="meta">${escape(row.handoff.length > 200 ? row.handoff.slice(0, 200) + "\u2026" : row.handoff)}</p>`}` +
|
|
15314
|
-
`<p class="meta mono">${escape(row.taskId)} \u00b7 ${
|
|
15373
|
+
`<p class="meta mono">${escape(row.taskId)} \u00b7 ${whenTime(row.completedAt)}${row.ranMinutes === null ? "" : ` \u00b7 ran ${row.ranMinutes}m`}${row.provider === null ? "" : ` \u00b7 ${escape(runCostWords({ authMode: row.authMode, costUsd: row.costUsd, tokensIn: null, tokensOut: null }))}`}` +
|
|
15315
15374
|
` \u00b7 <a href="${reviewHref(row.taskId)}">review \u2192</a></p></div>`);
|
|
15316
15375
|
})
|
|
15317
15376
|
.join("\n");
|
|
@@ -16748,7 +16807,7 @@ function routineScreenPage(chrome, data) {
|
|
|
16748
16807
|
`<p class="meta">${routine.paused ? "resuming fires again at the next due slot" : "pausing stops firing instantly; a running instance finishes"}${runNowForm === "" ? "" : " · run now spawns an extra instance without touching the schedule — spend outside the schedule takes your password again"}</p>` +
|
|
16749
16808
|
`</div>`;
|
|
16750
16809
|
const ledger = fires.length === 0
|
|
16751
|
-
? `<p class="meta">No firings yet${approved && routine.nextFireAt !== null ? ` — first at ${
|
|
16810
|
+
? `<p class="meta">No firings yet${approved && routine.nextFireAt !== null ? ` — first at ${whenTime(routine.nextFireAt)}` : ""}.</p>`
|
|
16752
16811
|
: fires
|
|
16753
16812
|
.map(fire => {
|
|
16754
16813
|
const said = fire.outcome === "fired"
|
|
@@ -16757,7 +16816,7 @@ function routineScreenPage(chrome, data) {
|
|
|
16757
16816
|
: `<a href="${taskHref(fire.instanceTaskId)}" class="mono">${escape(fire.instanceTaskId)}</a>${fire.instanceState === null ? "" : ` <span class="badge badge-${escape(fire.instanceState)}">${escape(fire.instanceState)}</span>`}`
|
|
16758
16817
|
: `<span class="meta">skipped — ${escape(fire.reason ?? "")}</span>`;
|
|
16759
16818
|
const slot = fire.scheduledFor.replace(/^manual:/, "");
|
|
16760
|
-
return `<p class="row">${said}<span class="right meta mono">${fire.reason === "manual" ? "run now · " : ""}${
|
|
16819
|
+
return `<p class="row">${said}<span class="right meta mono">${fire.reason === "manual" ? "run now · " : ""}${whenTime(slot)}</span></p>`;
|
|
16761
16820
|
})
|
|
16762
16821
|
.join("\n");
|
|
16763
16822
|
return screen(`routine · ${routine.name}`, [
|
|
@@ -16769,7 +16828,7 @@ function routineScreenPage(chrome, data) {
|
|
|
16769
16828
|
? `<div class="problem">stopped behind <a href="${taskHref(data.blocker.taskId)}" class="mono">${escape(data.blocker.taskId)}</a> (${escape(data.blocker.state)}) — the track resumes when it finishes or is cancelled</div>`
|
|
16770
16829
|
: "",
|
|
16771
16830
|
approved && !routine.paused && routine.nextFireAt !== null
|
|
16772
|
-
? `<p class="meta">next fire ${
|
|
16831
|
+
? `<p class="meta">next fire ${whenTime(routine.nextFireAt)}</p>`
|
|
16773
16832
|
: "",
|
|
16774
16833
|
"<h2>the standing order</h2>",
|
|
16775
16834
|
terms,
|
|
@@ -16863,8 +16922,8 @@ function homePage(chrome, data) {
|
|
|
16863
16922
|
? ""
|
|
16864
16923
|
: `<div class="stat-card"><span class="k"><span class="dot ${data.episode.endedAt === null ? "dot-ok pulse" : "dot-off"}"></span>Toolroll</span>` +
|
|
16865
16924
|
`<span class="v">${data.episode.endedAt === null
|
|
16866
|
-
? `running since ${
|
|
16867
|
-
: `last window: ${data.episode.built} built, ${data.episode.broke} broke \u00b7 ended ${
|
|
16925
|
+
? `running since ${whenTime(data.episode.startedAt)}`
|
|
16926
|
+
: `last window: ${data.episode.built} built, ${data.episode.broke} broke \u00b7 ended ${whenTime(data.episode.endedAt)}`}</span></div>`;
|
|
16868
16927
|
const fleetCards = [...runnerCards, watchCard, ...worktreeCards].filter(one => one !== "");
|
|
16869
16928
|
const fleet = fleetCards.length === 0
|
|
16870
16929
|
? `<h2>system status</h2><p class="hint">No builder is connected yet. On the machine where the project lives, open that folder and run <code>toolroll up</code>.</p>`
|
|
@@ -17545,7 +17604,7 @@ function contestPage(chrome, data) {
|
|
|
17545
17604
|
`only one result will be kept as the task's outcome; the rest stay archived with their evidence</p>`,
|
|
17546
17605
|
data.problem === null ? "" : `<div class="problem">${escape(data.problem)}</div>`,
|
|
17547
17606
|
`<p class="row"><strong>${escape((CONTEST_STATE_WORDS[contest.state] ?? "the tournament is in an unexpected state — the records have the detail").replace(/tournament/g, contestNoun(contest.kind)))}</strong></p>`,
|
|
17548
|
-
contest.pickedBy === null ? "" : `<p class="meta">picked by ${escape(contest.pickedBy)} at ${
|
|
17607
|
+
contest.pickedBy === null ? "" : `<p class="meta">picked by ${escape(contest.pickedBy)} at ${whenTime(contest.pickedAt ?? "")}</p>`,
|
|
17549
17608
|
contest.kind === "comparison"
|
|
17550
17609
|
? `<p class="row"><strong>spend</strong> ${escape(contestDollars(data.totalMicrousd))} measured on the lanes that report dollars` +
|
|
17551
17610
|
`${data.anyUnknown ? ` <span class="meta">— the rest report tokens only</span>` : ""}</p>`
|
|
@@ -17742,7 +17801,7 @@ function githubReposPage(chrome, data) {
|
|
|
17742
17801
|
`<div class="row"><strong>${escape(repo.nameWithOwner)}</strong>${repo.isPrivate ? ` <span class="badge">private</span>` : ""}`,
|
|
17743
17802
|
`<span class="right">${action}</span></div>`,
|
|
17744
17803
|
localPath === null
|
|
17745
|
-
? `<p class="meta">not on this machine yet${/^\d{4}-\d{2}-\d{2}T/.test(repo.updatedAt) ? ` · pushed ${
|
|
17804
|
+
? `<p class="meta">not on this machine yet${/^\d{4}-\d{2}-\d{2}T/.test(repo.updatedAt) ? ` · pushed ${whenTime(repo.updatedAt)}` : ""}</p>`
|
|
17746
17805
|
: `<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${escape(localPath)}</p>`,
|
|
17747
17806
|
repo.description === "" ? "" : `<p class="meta">${escape(repo.description)}</p>`,
|
|
17748
17807
|
`</div>`,
|
|
@@ -18443,7 +18502,7 @@ function planRevisionLedgerHtml(ledger, taskId, csrf) {
|
|
|
18443
18502
|
: `<details class="plan-revision-history"><summary>revision history (${ledger.history.length})</summary>` +
|
|
18444
18503
|
ledger.history
|
|
18445
18504
|
.map(one => `<p class="row"><span class="mono">rev ${one.revision}</span> <span class="badge">${escape(one.status)}</span> ` +
|
|
18446
|
-
`<span class="meta">${escape(one.author)} · ${
|
|
18505
|
+
`<span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> — ${escape(one.reason)}</p>`)
|
|
18447
18506
|
.join("\n") +
|
|
18448
18507
|
`</details>`;
|
|
18449
18508
|
return (`<div class="card plan-revision"><span class="eyebrow">plan updated</span><h2>Using plan revision ${current.revision}</h2>` +
|
|
@@ -18780,7 +18839,7 @@ function taskBodyParts(data) {
|
|
|
18780
18839
|
? `attached to build #${one.attachedRun} — delivery not yet proven`
|
|
18781
18840
|
: "waiting for the next attempt";
|
|
18782
18841
|
const steerRows = steering
|
|
18783
|
-
.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${
|
|
18842
|
+
.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)} · ${escape(steerState(one))}</span> ` +
|
|
18784
18843
|
`${escape(one.note)}</p>`)
|
|
18785
18844
|
.join("\n");
|
|
18786
18845
|
const steerForm = data.csrf === "" || task.state === "done" || task.state === "cancelled" || (data.contest ?? null) !== null
|
|
@@ -18796,7 +18855,7 @@ function taskBodyParts(data) {
|
|
|
18796
18855
|
: `<h2>holds</h2>` +
|
|
18797
18856
|
data.holds
|
|
18798
18857
|
.map(hold => `<p class="row">${escape(holdOwnerWords(hold.ownerKind))} — ${escape(hold.reason)}` +
|
|
18799
|
-
`${hold.until === null ? "" : ` <span class="meta">until ${
|
|
18858
|
+
`${hold.until === null ? "" : ` <span class="meta">until ${whenTime(hold.until)}</span>`}</p>`)
|
|
18800
18859
|
.join("\n") +
|
|
18801
18860
|
`<p class="meta">only your hold can be lifted here — waits caused by questions, incidents, or retry delays clear on their own</p>`;
|
|
18802
18861
|
const approval = approvalOf(scope);
|
|
@@ -18822,7 +18881,7 @@ function taskBodyParts(data) {
|
|
|
18822
18881
|
})(),
|
|
18823
18882
|
acceptanceCeremonyHtml(scope.acceptance),
|
|
18824
18883
|
approval.approved
|
|
18825
|
-
? `<p class="meta scope-seal">Approved by ${escape(approval.by)} · ${
|
|
18884
|
+
? `<p class="meta scope-seal">Approved by ${escape(approval.by)} · ${whenTime(approval.at)} · <span class="seal">signs ${shortDigest(scope.digest)}</span><span class="so-sr-only"> — approval binds to this exact wording</span></p>`
|
|
18826
18885
|
: `<p class="meta"><span class="seal">signs ${shortDigest(scope.digest)}</span> — approval binds to this exact wording</p>` +
|
|
18827
18886
|
`<p class="meta">Not approved${approval.reason === "changed" ? " — approved once, then rewritten" : ""}</p>`,
|
|
18828
18887
|
`</div>`,
|
|
@@ -19028,7 +19087,7 @@ function taskBodyParts(data) {
|
|
|
19028
19087
|
? [
|
|
19029
19088
|
`<div class="card" data-attended="${escape(attended.open.id)}">`,
|
|
19030
19089
|
`<p><strong>attended session</strong> <span class="meta">${escape(attended.open.state)}</span></p>`,
|
|
19031
|
-
`<p class="meta">${attended.open.running ? "the agent is running — your open console keeps it live" : "waiting to dispatch to this machine"} · ${attended.open.turnsUsed}/${attended.open.cap} messages · $${(attended.open.spentMicrousd / 1_000_000).toFixed(2)} of $${(attended.open.budgetMicrousd / 1_000_000).toFixed(2)} · everything ends by ${
|
|
19090
|
+
`<p class="meta">${attended.open.running ? "the agent is running — your open console keeps it live" : "waiting to dispatch to this machine"} · ${attended.open.turnsUsed}/${attended.open.cap} messages · $${(attended.open.spentMicrousd / 1_000_000).toFixed(2)} of $${(attended.open.budgetMicrousd / 1_000_000).toFixed(2)} · everything ends by ${whenTime(attended.open.expiresAt)}</p>`,
|
|
19032
19091
|
`<p class="meta">this page being open keeps it alive — close it and the session winds down within a minute</p>`,
|
|
19033
19092
|
`<form method="post" action="${taskHref(task.id)}/attend-revoke" class="inline">`,
|
|
19034
19093
|
`<input type="hidden" name="csrf" value="${escape(data.csrf)}">`,
|
|
@@ -19181,7 +19240,7 @@ function taskBodyParts(data) {
|
|
|
19181
19240
|
runOutcomeBadge(run, run.id === liveHistoryRunId) +
|
|
19182
19241
|
`${run.reason === null ? "" : ` <span class="meta">${escape(reasonWords(run.reason))}</span>`}` +
|
|
19183
19242
|
` <span class="meta mono">${escape(bits.join(" · "))}</span>` +
|
|
19184
|
-
`<span class="right meta mono">${
|
|
19243
|
+
`<span class="right meta mono">${whenTime(run.startedAt)}</span></p>`);
|
|
19185
19244
|
})
|
|
19186
19245
|
.join("\n");
|
|
19187
19246
|
// ---- the right rail (slice 1c) ------------------------------------------
|
|
@@ -19260,7 +19319,7 @@ function taskBodyParts(data) {
|
|
|
19260
19319
|
const scopeRow = scope === null
|
|
19261
19320
|
? prop("scope", "none yet")
|
|
19262
19321
|
: approval.approved
|
|
19263
|
-
? prop("approved scope", `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${escape(qualityModeTitle(scope.qualityMode ?? "default"))} · approved by ${escape(approval.by)} · ${
|
|
19322
|
+
? prop("approved scope", `<span class="seal">signs ${shortDigest(scope.digest)}</span> · ${escape(qualityModeTitle(scope.qualityMode ?? "default"))} · approved by ${escape(approval.by)} · ${whenTime(approval.at)}`)
|
|
19264
19323
|
: prop("scope", approval.reason === "changed" ? "rewritten since its approval — needs a new yes" : "not approved");
|
|
19265
19324
|
const strikesRow = data.strikes > 0 ? prop("strikes", `${data.strikes} failed attempt(s)`) : "";
|
|
19266
19325
|
const propsCard = `<div class="card props">${workerRow}${queueRow}${scopeRow}${publishesRow}${economics}${strikesRow}</div>`;
|
|
@@ -19624,6 +19683,11 @@ function taskBodyParts(data) {
|
|
|
19624
19683
|
{ id: "task-diagnostics", title: "Task options", html: optionsHtml, open: optionsOpen, count: null },
|
|
19625
19684
|
],
|
|
19626
19685
|
cancel: cancelForm === "" ? null : { html: cancelForm, open: data.cancelDraft !== undefined },
|
|
19686
|
+
// "Do this every time…": the starter flow that does this kind of work on its own, one yes away.
|
|
19687
|
+
everyTime: data.repo === null || data.csrf === "" ? null : (() => {
|
|
19688
|
+
const starter = starterForWork(`${task.title}\n${scope?.goal ?? ""}`);
|
|
19689
|
+
return { href: `/settings/flows?repo=${encodeURIComponent(data.repo)}&starter=${starter.id}#starter-${starter.id}`, starter: starter.name };
|
|
19690
|
+
})(),
|
|
19627
19691
|
};
|
|
19628
19692
|
return { html, view };
|
|
19629
19693
|
}
|
|
@@ -19966,7 +20030,7 @@ function reviewCockpitPage(chrome, data) {
|
|
|
19966
20030
|
: `<span class="cockpit-why">${statusLineHtml(status)}</span>`;
|
|
19967
20031
|
return (`<li data-review-priority="${row.priority.band}"><a class="cockpit-row${current ? " current" : ""}" href="${escape(reviewHref(row.taskId, row.runId, row.repo))}"${current ? ` aria-current="page"` : ""}>` +
|
|
19968
20032
|
`<span class="cockpit-row-head"><strong>${escape(row.title)}</strong></span>` +
|
|
19969
|
-
`<span class="cockpit-row-meta">${
|
|
20033
|
+
`<span class="cockpit-row-meta">${whenTime(row.completedAt)}${row.runId === null ? " · no build" : row.outcome === "no-change" ? " · no change" : ""}${row.prNumber === null ? "" : ` · PR #${row.prNumber}`}</span>` +
|
|
19970
20034
|
(row.historyProblem ? `<span class="cockpit-why" data-history-problem>History unavailable</span>` : why) +
|
|
19971
20035
|
(status !== null && row.ciFailing ? `<span class="cockpit-why">CI is failing</span>` : "") +
|
|
19972
20036
|
`</a></li>`);
|
|
@@ -20054,7 +20118,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
20054
20118
|
? "the scope changed after approval — the words below are the current text, not the signed one"
|
|
20055
20119
|
: "never approved — the result was built without a signed scope";
|
|
20056
20120
|
const approval = intent.approval.approved
|
|
20057
|
-
? `approved by ${escape(intent.approvedBy ?? "an operator")} · ${
|
|
20121
|
+
? `approved by ${escape(intent.approvedBy ?? "an operator")} · ${whenTime(intent.approval.at)}`
|
|
20058
20122
|
: approvalWords;
|
|
20059
20123
|
const body = `<p class="recap" style="margin-top:.25rem"><strong>goal</strong> ${escape(intent.goal)}</p>` +
|
|
20060
20124
|
(intent.outOfScope === null ? `<p class="meta">no boundary was stated</p>` : `<p class="recap"><strong>not this</strong> ${escape(intent.outOfScope)}</p>`) +
|
|
@@ -20116,7 +20180,7 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
20116
20180
|
parts.push(completionForm(view.taskId, run.id, complete.receipt, csrf, view.detail?.pullRequestTo ?? null));
|
|
20117
20181
|
if (view.notes.length > 0) {
|
|
20118
20182
|
parts.push(`<section class="card cockpit-section" data-cockpit-section="notes"><h3>operator notes</h3>` +
|
|
20119
|
-
view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${
|
|
20183
|
+
view.notes.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ${escape(one.note)}</p>`).join("\n") +
|
|
20120
20184
|
`</section>`);
|
|
20121
20185
|
}
|
|
20122
20186
|
return {
|
|
@@ -20312,7 +20376,7 @@ function runsPage(chrome, rows, liveIds, nextCursor, verdicts = new Map(), accep
|
|
|
20312
20376
|
needsVerification +
|
|
20313
20377
|
criterionMatrixSummary(verdicts.get(run.id)?.matrix ?? []) +
|
|
20314
20378
|
`${run.provider === "claude" ? "" : ` <span class="meta mono">${escape(run.provider)}</span>`}` +
|
|
20315
|
-
`<span class="right meta mono">${
|
|
20379
|
+
`<span class="right meta mono">${whenTime(run.startedAt)}` +
|
|
20316
20380
|
`${run.providerStartedAt === null && run.tokensIn === null && run.tokensOut === null && run.costUsd === null ? "" : ` \u00b7 ${escape(runCostWords(run, liveIds.has(run.id)))}`}</span></p>`);
|
|
20317
20381
|
})
|
|
20318
20382
|
.join("\n");
|
|
@@ -20809,7 +20873,7 @@ function evidenceBundleCard(view, run, publication = null, review = null) {
|
|
|
20809
20873
|
parts.push(`<p class="row" data-proof-verdict="${escape(dispatchStatusToken(view.verdict))}">${statusLineHtml(status)} <span class="meta">${escape(verificationExplanation(view.verdict, view.reasons))}</span></p>`);
|
|
20810
20874
|
}
|
|
20811
20875
|
if (view.accepted !== null) {
|
|
20812
|
-
parts.push(`<p class="meta">Accepted with an exception by <span class="mono">${escape(view.accepted.by)}</span> · ${
|
|
20876
|
+
parts.push(`<p class="meta">Accepted with an exception by <span class="mono">${escape(view.accepted.by)}</span> · ${whenTime(view.accepted.at)}${view.accepted.note === null ? "" : ` — ${escape(view.accepted.note)}`}</p>`);
|
|
20813
20877
|
}
|
|
20814
20878
|
parts.push(criterionMatrixHtml(view.matrix, { runId, links: view.matrixLinks }));
|
|
20815
20879
|
parts.push(semanticCoverageHtml(view.matrix, view.qualityMode));
|
|
@@ -21085,7 +21149,7 @@ function resultPanelParts(detail, o) {
|
|
|
21085
21149
|
(publication === null || publication.prNumber === null
|
|
21086
21150
|
? ""
|
|
21087
21151
|
: ` ${prUrl === null ? `<span class="mono">PR #${publication.prNumber}</span>` : `<a href="${escape(prUrl)}">PR #${publication.prNumber}</a>`}` +
|
|
21088
|
-
`<span class="meta" data-ci-observed="${escape(detail.ciFailing ? "failing" : (publication.lastCheckState ?? "none"))}"> · ${detail.ciFailing ? "CI failing at the last check" : publication.lastCheckState === "passing" ? `CI passing, observed ${
|
|
21152
|
+
`<span class="meta" data-ci-observed="${escape(detail.ciFailing ? "failing" : (publication.lastCheckState ?? "none"))}"> · ${detail.ciFailing ? "CI failing at the last check" : publication.lastCheckState === "passing" ? `CI passing, observed ${whenTime(publication.lastCheckAt)}` : publication.lastCheckState === "running" ? "CI still running at the last check" : "no CI checks found — verify on GitHub"}</span>`) +
|
|
21089
21153
|
`</p>`;
|
|
21090
21154
|
const agent = runAgentWords(detail.route);
|
|
21091
21155
|
const links = [];
|
|
@@ -21101,7 +21165,7 @@ function resultPanelParts(detail, o) {
|
|
|
21101
21165
|
summaryParts.push(`<details class="result-details" data-result-details><summary>Build details</summary>` +
|
|
21102
21166
|
(unavailableShots.length === 0 ? "" : `<ul class="result-unavailable meta" data-result-unavailable="${unavailableShots.length}">${unavailableShots.map(one => `<li>Screenshot unavailable — <span class="mono">${escape(one.path)}</span>: ${escape(one.problem ?? "")}</li>`).join("")}</ul>`) +
|
|
21103
21167
|
`<dl class="result-facts">` +
|
|
21104
|
-
`<div><dt>Build</dt><dd>#${runId} · ${escape(run.runner)}${agent === null ? ` · ${escape(run.provider)}` : ""}${run.finishedAt === null ? "" : ` · ${
|
|
21168
|
+
`<div><dt>Build</dt><dd>#${runId} · ${escape(run.runner)}${agent === null ? ` · ${escape(run.provider)}` : ""}${run.finishedAt === null ? "" : ` · ${whenTime(run.finishedAt)}`}</dd></div>` +
|
|
21105
21169
|
(agent === null ? "" : `<div><dt>Agent</dt><dd>${escape(agent)}</dd></div>`) +
|
|
21106
21170
|
`<div><dt>Commits</dt><dd>${facts.head === null
|
|
21107
21171
|
? facts.base === null ? "no commit recorded" : `<span class="mono">${escape(facts.base.slice(0, 12))}</span> <span class="meta">base · no head recorded</span>`
|
|
@@ -21180,7 +21244,7 @@ function resultPanelParts(detail, o) {
|
|
|
21180
21244
|
checkParts.push(`<p class="meta" data-proof-verdict="none">No verification result is available for this build.</p>`);
|
|
21181
21245
|
}
|
|
21182
21246
|
if (proof.accepted !== null)
|
|
21183
|
-
checkParts.push(`<p class="meta">${humanReview ? "Accepted after human review" : "Accepted with an exception"} by <span class="mono">${escape(proof.accepted.by)}</span> · ${
|
|
21247
|
+
checkParts.push(`<p class="meta">${humanReview ? "Accepted after human review" : "Accepted with an exception"} by <span class="mono">${escape(proof.accepted.by)}</span> · ${whenTime(proof.accepted.at)}${proof.accepted.note === null ? "" : ` — ${escape(proof.accepted.note)}`}</p>`);
|
|
21184
21248
|
if (proof.matrix.length === 0) {
|
|
21185
21249
|
checkParts.push(detail.signedCriteria > 0 ? `<p class="meta">The approved scope has ${detail.signedCriteria} requirement${detail.signedCriteria === 1 ? "" : "s"}, but this build has no requirement-by-requirement verification.</p>` : `<p class="meta">This scope signed no acceptance checks.</p>`);
|
|
21186
21250
|
}
|
|
@@ -21231,7 +21295,7 @@ function resultPanelParts(detail, o) {
|
|
|
21231
21295
|
};
|
|
21232
21296
|
if (detail.comments.length > 0) {
|
|
21233
21297
|
requestParts.push(`<p class="meta">Saved for later · ${detail.comments.length}</p><div class="diff-comments" data-result-notes="${detail.comments.length}">` +
|
|
21234
|
-
detail.comments.map(one => `<div class="diff-comment"><span class="diff-comment-pin" aria-hidden="true"></span><p>${one.path === null ? "" : pathWords(one.path, one.line)}${escape(one.note)}</p><span class="meta">${escape(one.author)} · ${
|
|
21298
|
+
detail.comments.map(one => `<div class="diff-comment"><span class="diff-comment-pin" aria-hidden="true"></span><p>${one.path === null ? "" : pathWords(one.path, one.line)}${escape(one.note)}</p><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span></div>`).join("") +
|
|
21235
21299
|
`</div>`);
|
|
21236
21300
|
if (o.csrf !== "" && !detail.canAnnotate) {
|
|
21237
21301
|
requestParts.push(`<form method="post" action="/r/${runId}/revise" class="card revision-from-comments"><input type="hidden" name="csrf" value="${escape(o.csrf)}"><input type="hidden" name="return" value="${escape(o.returnTo)}">${revisionSealFields(detail.comments, detail.sourceDigest)}` +
|
|
@@ -21240,7 +21304,7 @@ function resultPanelParts(detail, o) {
|
|
|
21240
21304
|
}
|
|
21241
21305
|
}
|
|
21242
21306
|
if ((detail.pastComments?.length ?? 0) > 0) {
|
|
21243
|
-
requestParts.push(`<details class="result-feedback-history"><summary>Earlier feedback</summary><div class="diff-comments" data-past-feedback>${detail.pastComments.map(one => `<div class="diff-comment"><p>${one.path === null ? "" : pathWords(one.path, one.line)}${escape(one.note)}</p><span class="meta">${escape(one.author)} · ${
|
|
21307
|
+
requestParts.push(`<details class="result-feedback-history"><summary>Earlier feedback</summary><div class="diff-comments" data-past-feedback>${detail.pastComments.map(one => `<div class="diff-comment"><p>${one.path === null ? "" : pathWords(one.path, one.line)}${escape(one.note)}</p><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span></div>`).join("")}</div></details>`);
|
|
21244
21308
|
}
|
|
21245
21309
|
for (const revision of detail.revisions) {
|
|
21246
21310
|
// The child's standing is the shared status projection's own words
|
|
@@ -21459,7 +21523,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
21459
21523
|
? "the brief"
|
|
21460
21524
|
: turn.sourceKind === "answer"
|
|
21461
21525
|
? "your answer"
|
|
21462
|
-
: "repair (machine)"} · ${
|
|
21526
|
+
: "repair (machine)"} · ${whenTime(turn.recordedAt)}${turnWords(turn)}</span> ${escape(oneLineOf(turn.text, 240))}</p>`)
|
|
21463
21527
|
.join("\n")) +
|
|
21464
21528
|
(heldTurns.open && csrf !== ""
|
|
21465
21529
|
? `<form method="post" action="/r/${run.id}/turn" class="row">` +
|
|
@@ -21534,7 +21598,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
21534
21598
|
const commentRows = comments
|
|
21535
21599
|
.map(one => `<div class="diff-comment"><span class="diff-comment-pin" aria-hidden="true"></span><p>` +
|
|
21536
21600
|
`${one.path === null ? "" : commentPathWords(one.path, one.line)}` +
|
|
21537
|
-
`${escape(one.note)}</p><span class="meta">${escape(one.author)} · ${
|
|
21601
|
+
`${escape(one.note)}</p><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span></div>`)
|
|
21538
21602
|
.join("\n");
|
|
21539
21603
|
const commentForm = csrf === "" || !hasTerminalDiff
|
|
21540
21604
|
? ""
|
|
@@ -21578,7 +21642,7 @@ function runPage(chrome, run, taskId, artifacts, terminal = null, notes = [], cs
|
|
|
21578
21642
|
const noteRows = notes.length === 0
|
|
21579
21643
|
? ""
|
|
21580
21644
|
: notes
|
|
21581
|
-
.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${
|
|
21645
|
+
.map(one => `<p class="row"><span class="meta">${escape(one.author)} · ${whenTime(one.createdAt)}</span> ` +
|
|
21582
21646
|
`${escape(one.note)}</p>`)
|
|
21583
21647
|
.join("\n");
|
|
21584
21648
|
const noteForm = csrf === ""
|
|
@@ -21779,7 +21843,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
21779
21843
|
: `<div class="card"><p><strong>${permissionDefault.mode === "bypassPermissions" ? "Full access" : "Auto"}</strong></p><p class="meta">an approver can change this default</p></div>`,
|
|
21780
21844
|
permissionDefault.updatedAt === null
|
|
21781
21845
|
? ""
|
|
21782
|
-
: `<p class="meta settings-changed">Changed ${
|
|
21846
|
+
: `<p class="meta settings-changed">Changed ${whenTime(permissionDefault.updatedAt)}${permissionDefault.updatedBy === null ? "" : ` by ${escape(permissionDefault.updatedBy)}`}</p>`,
|
|
21783
21847
|
].join("\n");
|
|
21784
21848
|
const qualityCard = qualityDefault === null
|
|
21785
21849
|
? ""
|
|
@@ -21793,7 +21857,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
21793
21857
|
: `<div class="card"><p><strong>${escape(qualityModeTitle(qualityDefault.mode))}</strong></p><p class="meta">an approver can change this default</p></div>`,
|
|
21794
21858
|
qualityDefault.updatedAt === null
|
|
21795
21859
|
? ""
|
|
21796
|
-
: `<p class="meta settings-changed">Changed ${
|
|
21860
|
+
: `<p class="meta settings-changed">Changed ${whenTime(qualityDefault.updatedAt)}${qualityDefault.updatedBy === null ? "" : ` by ${escape(qualityDefault.updatedBy)}`}</p>`,
|
|
21797
21861
|
].join("\n");
|
|
21798
21862
|
// Quiet chat: this person's own choice. The installation's Telegram cadence only bundles Every step updates.
|
|
21799
21863
|
const chatCard = chatNotices === null || csrf === ""
|
|
@@ -21896,7 +21960,7 @@ function settingsPage(chrome, existing, hasEnv, csrf, problem, messaging = null,
|
|
|
21896
21960
|
: `<p class="meta">Alerts need a secure (https) address for this app.</p>`,
|
|
21897
21961
|
...push.devices
|
|
21898
21962
|
.filter(one => one.retiredAt === null || one.retiredReason === "gone")
|
|
21899
|
-
.map(one => `<p class="row">${escape(one.uaWords)} · since ${
|
|
21963
|
+
.map(one => `<p class="row">${escape(one.uaWords)} · since ${whenTime(one.createdAt)}` +
|
|
21900
21964
|
`${one.retiredAt !== null ? ` · <span class="meta">expired</span>` : one.consecutiveFailures >= 20 ? ` · <span class="meta">failing</span>` : ""}` +
|
|
21901
21965
|
(one.retiredAt === null
|
|
21902
21966
|
? ` <form method="post" action="/push/remove" class="inline"><input type="hidden" name="csrf" value="${escape(csrf)}"><input type="hidden" name="id" value="${one.id}"><button type="submit">Remove</button></form>`
|
|
@@ -22089,6 +22153,7 @@ function settingsTiles() {
|
|
|
22089
22153
|
return `<nav class="settings-tiles" aria-label="Settings sections">${tiles.map(([href, label, icon]) => `<a href="${href}">${strokeIcon(icon)}<span>${label}</span></a>`).join("")}</nav>`;
|
|
22090
22154
|
}
|
|
22091
22155
|
const SETTINGS_TILE_ICONS = [
|
|
22156
|
+
["/settings/flows", "Flows", `<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/><path d="M10 6.5h4a3 3 0 0 1 3 3V14"/>`],
|
|
22092
22157
|
["/settings/integrations", "Integrations", `<path d="M9 2v6M15 2v6"/><path d="M6 8h12v4a6 6 0 0 1-12 0z"/><path d="M12 18v4"/>`],
|
|
22093
22158
|
["/settings/models", "Models", `<rect x="4" y="4" width="16" height="16" rx="2"/><rect x="9" y="9" width="6" height="6"/><path d="M9 2v2M15 2v2M9 20v2M15 20v2M2 9h2M2 15h2M20 9h2M20 15h2"/>`],
|
|
22094
22159
|
["/settings/skills", "Skills", `<path d="m12 3 1.9 5.8L20 10l-5 3.6L16.8 20 12 16.4 7.2 20 9 13.6 4 10l6.1-1.2z"/>`],
|
|
@@ -22150,7 +22215,7 @@ function nextPage(chrome, data) {
|
|
|
22150
22215
|
if (item.kind === "decision") {
|
|
22151
22216
|
const { decision } = item;
|
|
22152
22217
|
card =
|
|
22153
|
-
`<h1>${escape(decision.taskId)} <span class="meta">asked ${
|
|
22218
|
+
`<h1>${escape(decision.taskId)} <span class="meta">asked ${whenTime(decision.createdAt)}</span></h1>` +
|
|
22154
22219
|
`<div class="recap">${escape(decision.recap)}</div>` +
|
|
22155
22220
|
`<div class="question">${escape(decision.question)}</div>` +
|
|
22156
22221
|
decisionOptionForms(decision, data.csrf, "next");
|
package/dist/skills-ui.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { whenUtc } from "./when-html.js";
|
|
2
3
|
const e = (value) => String(value ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
3
4
|
const hidden = (data) => Object.entries(data)
|
|
4
5
|
.map(([k, v]) => `<input type="hidden" name="${e(k)}" value="${e(v)}">`)
|
|
@@ -41,7 +42,7 @@ export function skillsHtml(view, csrf, canManage, options = {}) {
|
|
|
41
42
|
? `<details class="card"${options.error && options.draft?.["method"] ? " open" : ""}><summary>Add skill</summary><form method="post" action="/settings/skills/import" data-skill-import>${hidden(base)}<label>Add from<select name="method" data-skill-method><option value="paste"${d["method"] === "paste" ? " selected" : ""}>Paste SKILL.md</option><option value="github"${d["method"] === "github" ? " selected" : ""}>GitHub folder</option><option value="folder"${d["method"] === "folder" ? " selected" : ""}>Local folder</option></select></label><div data-skill-input="paste"><label>SKILL.md<textarea name="content" rows="8" maxlength="24576" placeholder="--- name: design-review description: Review interface copy and layout. --- Write your instructions here.">${e(d["content"])}</textarea></label></div><div data-skill-input="github"><label>Public GitHub folder<input name="url" type="url" value="${e(d["url"])}" placeholder="https://github.com/owner/repo/tree/main/skills/review"></label></div><div data-skill-input="folder"><label>Skill folder<input type="file" webkitdirectory multiple data-skill-folder></label><input type="hidden" name="files" value=""><p class="meta">Choose the folder containing SKILL.md. Up to 64 files, 1 MB total.</p><noscript>Folder uploads require JavaScript. You can paste SKILL.md or use a GitHub folder.</noscript></div><p class="meta">Added to your library first. Review its source and requirements before enabling it.</p><p data-skill-error role="alert" hidden></p><button>Add to library</button></form></details>`
|
|
42
43
|
: "";
|
|
43
44
|
const history = view.history.length
|
|
44
|
-
? `<details><summary>Change history</summary>${view.history.map((h) => `<div class="card"><p>Selection ${h.revision} · ${e(h.actor)} · ${
|
|
45
|
+
? `<details><summary>Change history</summary>${view.history.map((h) => `<div class="card"><p>Selection ${h.revision} · ${e(h.actor)} · ${whenUtc(h.at)}</p><ul>${h.enabled.length ? h.enabled.map((s) => `<li>${e(s.name)} · ${e(s.version)}</li>`).join("") : "<li>No skills enabled</li>"}</ul>${manage && h.revision !== view.revision ? form("restore", "<button>Restore selection</button>", { restore: h.revision }) : '<span class="meta">Current selection</span>'}</div>`).join("")}</details>`
|
|
45
46
|
: "";
|
|
46
47
|
return `<section class="skills">${options.error ? `<p class="problem" role="alert">${e(options.error)}</p>` : ""}<p>${active ? `${active} skill${active === 1 ? "" : "s"} enabled for new tasks.` : "No skills enabled for this project."} Active tasks keep their saved versions.</p>${add}${search}${cards || "<p>Your library is empty. Add a skill to review and enable it here.</p>"}${history}</section>`;
|
|
47
48
|
}
|
package/dist/store.js
CHANGED
|
@@ -3770,6 +3770,8 @@ function initializeStore(db, file) {
|
|
|
3770
3770
|
addColumn(db, "publication_grant", "publish_on", "TEXT NOT NULL DEFAULT 'build' CHECK (publish_on IN ('build', 'complete'))");
|
|
3771
3771
|
addColumn(db, "publication_grant", "merge_when_green", "INTEGER NOT NULL DEFAULT 0 CHECK (merge_when_green IN (0, 1))");
|
|
3772
3772
|
db.exec(PULL_REQUEST_SCHEMA);
|
|
3773
|
+
// A pull request a flow's Pull request zone opened: the zone follows its CI, so Complete's follower leaves it alone.
|
|
3774
|
+
addColumn(db, "pull_request_follow", "flow_card", "INTEGER REFERENCES flow_card(id)");
|
|
3773
3775
|
addColumn(db, "flow_card", "source_json", "TEXT");
|
|
3774
3776
|
addColumn(db, "flow_card", "owner", "TEXT");
|
|
3775
3777
|
addColumn(db, "flow", "owner", "TEXT");
|
package/dist/surface.js
CHANGED
|
@@ -351,6 +351,6 @@ export const COMMAND_GUIDE = [
|
|
|
351
351
|
operator("serve", "the operations console (HTTP)"),
|
|
352
352
|
operator("watch", "the unattended loop, kept running"),
|
|
353
353
|
operator("up", "console + worker + browser, one command"),
|
|
354
|
-
operator("onboard", "add this repository, report signed-in agents, install or --remove the operator skill (--yes), and print the handoff"),
|
|
354
|
+
operator("onboard", "add this repository, report signed-in agents, install or --remove the operator skill (--yes), offer the starter flows (--starter <ids>), and print the handoff"),
|
|
355
355
|
operator("daemon", "install the loop under the OS service manager"),
|
|
356
356
|
];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** "16:39" today, "Yesterday 16:39" / "Tomorrow 16:39" beside it, "Sep 28" otherwise ("Sep 28 2025" in another year). */
|
|
2
|
+
export declare function shortWhen(iso: string, now?: Date): string;
|
|
3
|
+
/** The `<time>` for a stamp: `full` is the words a desk shows ("2026-09-30 16:39 UTC"). Empty for no stamp. */
|
|
4
|
+
export declare function whenHtml(iso: string | null, full: string, now?: Date): string;
|
|
5
|
+
/** "2026-09-30 16:39 UTC" — the desk's words for a stamp. */
|
|
6
|
+
export declare const utcMinute: (iso: string) => string;
|
|
7
|
+
/** Both the desk and phone forms of a UTC stamp. */
|
|
8
|
+
export declare const whenUtc: (iso: string | null, now?: Date) => string;
|