toolroll 0.9.100 → 0.9.101
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 +7 -0
- package/dist/accent-colors.js +1 -1
- package/dist/activity-line.d.ts +1 -1
- package/dist/activity-line.js +1 -1
- package/dist/browser/terminal.css +1 -1
- package/dist/browser/terminal.js +42 -42
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +32 -32
- package/dist/browser-workspace.d.ts +31 -2
- package/dist/design/kit.css +94 -2
- package/dist/design/kit.d.ts +12 -4
- package/dist/design/kit.js +8 -2
- package/dist/design/tokens.css +5 -5
- package/dist/first-run.d.ts +10 -6
- package/dist/first-run.js +19 -18
- package/dist/guides.js +1 -1
- package/dist/operate.js +2 -0
- package/dist/scope.d.ts +5 -0
- package/dist/scope.js +8 -1
- package/dist/server/chrome.d.ts +1 -1
- package/dist/server/chrome.js +1 -1
- package/dist/server/console.css +46 -17
- package/dist/server/kit.d.ts +36 -3
- package/dist/server/kit.js +33 -4
- package/dist/server/render-chat.d.ts +2 -0
- package/dist/server/render-chat.js +1 -0
- package/dist/server/render-pages.js +1 -1
- package/dist/server/render-results.js +3 -3
- package/dist/server/render-tasks.d.ts +10 -0
- package/dist/server/render-tasks.js +231 -62
- package/dist/server/task-views.js +5 -1
- package/dist/settings-groups-ui.js +1 -1
- package/dist/store.js +14 -3
- package/dist/task-archive.d.ts +3 -0
- package/dist/task-archive.js +5 -0
- package/dist/task-status.d.ts +2 -2
- package/dist/task-status.js +3 -3
- package/dist/tui.js +1 -1
- package/dist/workspace-ui.d.ts +69 -0
- package/dist/workspace-ui.js +103 -0
- package/package.json +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/** Task pages: approval, agents, plans, controls, decisions and the task body. */
|
|
2
2
|
import { archivedWords } from "../tidy.js";
|
|
3
|
-
import { btn } from "./kit.js";
|
|
3
|
+
import { btn, stepRows } from "./kit.js";
|
|
4
4
|
import { whenUtc } from "../when-html.js";
|
|
5
5
|
import { networkWords } from "../egress.js";
|
|
6
6
|
import { wasInterrupted } from "../run-ending.js";
|
|
7
|
-
import { chainTermsLines, liveChainFromJson, releaseAsksAgainWords, releaseTermsSentence } from "../scope.js";
|
|
7
|
+
import { chainTermsLines, liveChainFromJson, releaseAsksAgainWords, releaseCheckSentence, releaseTermsSentence } from "../scope.js";
|
|
8
8
|
import { SUBSCRIPTION_CAPABLE } from "../keys.js";
|
|
9
9
|
import { RELEASE_CI_RUNNING_WORDS } from "../release-ci-hold.js";
|
|
10
10
|
import { writesWords } from "../writes-mode.js";
|
|
@@ -47,10 +47,10 @@ import {} from "../store.js";
|
|
|
47
47
|
import { runCostWords } from "../summary.js";
|
|
48
48
|
import {} from "../task-control.js";
|
|
49
49
|
import { assignmentStageOf, demoChecksOf, stageOfCode, stageOfDispatch, STATUS_MORE, taskStatusOf } from "../task-status.js";
|
|
50
|
-
import { buildProgressOf, dispatchActionLabel, earlierAttemptsWords, receiptPublicationWords, resultStatusOf, REVIEW_TOKENS, workStatusOf } from "../workspace-ui.js";
|
|
50
|
+
import { buildProgressOf, dispatchActionLabel, earlierAttemptsWords, receiptPublicationWords, resultStatusOf, REVIEW_TOKENS, stepRowsOf, workStatusOf } from "../workspace-ui.js";
|
|
51
51
|
import { createHash, randomUUID } from "node:crypto";
|
|
52
52
|
import { TEXT_LIMITS } from "../text-limits.js";
|
|
53
|
-
import { isOverdue, oneLineOf, projectChip, regionScript, safePrUrl, screen, SENSITIVE_INPUT, sentenceCase, shortDigest,
|
|
53
|
+
import { isOverdue, oneLineOf, projectChip, regionScript, safePrUrl, screen, SENSITIVE_INPUT, sentenceCase, shortDigest, whenTime } from "./chrome.js";
|
|
54
54
|
import { steerDestinationWords, steerStandingParts } from "../steer-status.js";
|
|
55
55
|
import { reviewHref, taskChatHref, taskHref } from "./http.js";
|
|
56
56
|
import { coordinatorProposalsSection } from "./render-chat.js";
|
|
@@ -313,6 +313,9 @@ export function approvalSheetParts(input) {
|
|
|
313
313
|
// What the yes allows, in plain words, right above it: the agent's
|
|
314
314
|
// permissions, the per-attempt limit, and an earlier version still running.
|
|
315
315
|
const allowing = [];
|
|
316
|
+
// The clauses of `allowing` the plan sheet says under Worth knowing (a paid fallback, an earlier version still
|
|
317
|
+
// running): warnings, said first, never folded.
|
|
318
|
+
const warnings = [];
|
|
316
319
|
const allowed = permissionPlainWords(scope.profile);
|
|
317
320
|
if (allowed !== null)
|
|
318
321
|
allowing.push(allowed);
|
|
@@ -323,11 +326,16 @@ export function approvalSheetParts(input) {
|
|
|
323
326
|
if (fallbacks.length > 0) {
|
|
324
327
|
const paid = fallbacks.filter(one => one.authMode === "api-key" || SUBSCRIPTION_CAPABLE[one.profile.provider] !== true);
|
|
325
328
|
// Each agent named once, the paid one said where it stands in the order.
|
|
326
|
-
|
|
329
|
+
const clause = `if it runs out of its plan, the work moves to ${fallbacks.map(one => `${agentNameWords(one.profile.provider, one.profile.model)}${paid.includes(one) ? " on an API key, which spends money" : ""}`).join(", then ")}`;
|
|
330
|
+
allowing.push(clause);
|
|
331
|
+
if (paid.length > 0)
|
|
332
|
+
warnings.push(clause);
|
|
327
333
|
}
|
|
328
334
|
const earlier = earlierVersionsWords(input.earlier.active, input.earlier.running);
|
|
329
|
-
if (earlier !== null)
|
|
335
|
+
if (earlier !== null) {
|
|
330
336
|
allowing.push(earlier);
|
|
337
|
+
warnings.push(earlier);
|
|
338
|
+
}
|
|
331
339
|
const yoursToCheck = scope.acceptance.filter(one => one.evidence.includes("manual-review")).map(one => one.statement.trim().replace(/[.\s]+$/, ""));
|
|
332
340
|
const consent = joinHtml([
|
|
333
341
|
allowing.length === 0 ? "" : html `<p class="approval-allowing" data-approval-allowing>You’re allowing: ${allowing.join(" · ")}</p>`,
|
|
@@ -342,13 +350,14 @@ export function approvalSheetParts(input) {
|
|
|
342
350
|
const submit = rebuild !== null ? html `Approve and retry` : html `Approve`;
|
|
343
351
|
// Everything else the digest binds, one tap away.
|
|
344
352
|
const detail = (title, body, attrs = html ``) => isEmptyHtml(body) ? html `` : html `<section class="approval-detail"${attrs}><h3>${title}</h3>${body}</section>`;
|
|
345
|
-
const
|
|
353
|
+
const revisionBody = input.revision === null ? html `` : joinHtml([
|
|
346
354
|
html `<div class="revision-card" data-revision-feedback>`,
|
|
347
355
|
html `<p><a href="${input.revisionSourceHref}"${input.surface === "chat" ? html ` data-revision-source` : ""}>${input.surface === "chat" ? html `Original result: build #${input.revision.sourceRun} →` : html `build #${input.revision.sourceRun}`}</a></p>`,
|
|
348
356
|
input.revision.comments.length === 0 ? "" : html `<ul>${input.revision.comments.map(one => html `<li>${revisionImageLink(one.image)}${one.path === null ? "" : html `<span class="mono">${one.path}${one.line === null ? "" : `:${one.line}`}</span> · `}${one.note} <span class="meta">— ${one.author}</span></li>`)}</ul>`,
|
|
349
357
|
revisionLineageHtml(input.revision.lineage),
|
|
350
358
|
html `</div>`,
|
|
351
|
-
])
|
|
359
|
+
]);
|
|
360
|
+
const revisionDetail = input.revision === null ? "" : detail("Earlier build", revisionBody);
|
|
352
361
|
const contract = input.planContract;
|
|
353
362
|
const amendment = approvalAmendmentHtml(contract);
|
|
354
363
|
const contractDetail = input.planDocument === null || !isEmptyHtml(amendment) ? html ``
|
|
@@ -375,7 +384,8 @@ export function approvalSheetParts(input) {
|
|
|
375
384
|
scope.budgetMicrousd === null ? "" : html `<p class="meta">Each build attempt has a ${money(scope.budgetMicrousd)} agent-reported usage cap. On a subscription this limits work; it is not an API charge.</p>`,
|
|
376
385
|
]);
|
|
377
386
|
const details = joinHtml([
|
|
378
|
-
|
|
387
|
+
// On the task page the details are open: the page without script shows every term before the yes. Chat keeps the fold.
|
|
388
|
+
html `<details class="approval-details"${input.surface === "task" ? html ` open` : ""}><summary>Plan details</summary>`,
|
|
379
389
|
revisionDetail,
|
|
380
390
|
repairText === null ? "" : detail("Full goal", html `<p>${scope.goal}</p>`),
|
|
381
391
|
detail("Plan record", contractDetail),
|
|
@@ -445,12 +455,21 @@ export function approvalSheetParts(input) {
|
|
|
445
455
|
input.deliverable === "report" ? html `<p class="approval-note">Read-only: it reports back and changes nothing in the repository.</p>` : "",
|
|
446
456
|
originNote(input.origin) === null ? "" : html `<p class="approval-note" data-approval-origin>${originNote(input.origin)}</p>`,
|
|
447
457
|
]);
|
|
458
|
+
// A release's terms are part of what the yes agrees to (the digest signs them): said whole on every approval
|
|
459
|
+
// surface (the plan sheet, the page without script, chat), before Approve, with nothing folded.
|
|
460
|
+
const releaseBody = scope.releaseTerms == null && scope.releaseCheck == null ? "" : joinHtml([
|
|
461
|
+
scope.releaseTerms == null ? "" : html `<p>${releaseTermsSentence(scope.releaseTerms)}</p>`,
|
|
462
|
+
scope.releaseCheck == null ? "" : html `<p>${releaseCheckSentence(scope.releaseCheck)}</p>`,
|
|
463
|
+
input.releaseCiRunning === true ? html `<p class="meta">${RELEASE_CI_RUNNING_WORDS}</p>` : "",
|
|
464
|
+
scope.releaseTerms?.policy.carry !== true ? "" : html `<p class="plan-sheet-sub">What asks again</p><ul class="plan-sheet-list">${releaseAsksAgainWords(scope.releaseTerms).map(line => html `<li>${line}</li>`)}</ul>`,
|
|
465
|
+
]);
|
|
448
466
|
const rows = joinHtml([
|
|
449
467
|
html `<dl class="approval-rows">`,
|
|
450
468
|
row("Goal", html `<p class="approval-goal">${goalShown}</p>`),
|
|
451
469
|
row("Changes", changes),
|
|
452
470
|
row("Won’t touch", html `<p>${scope.outOfScope === null ? "Nothing is ruled out." : scope.outOfScope}</p>`),
|
|
453
471
|
row("Done when", doneWhen),
|
|
472
|
+
releaseBody === "" ? "" : html `<div class="approval-row" data-approval-release><dt>Release</dt><dd>${releaseBody}</dd></div>`,
|
|
454
473
|
chain.length === 0 ? "" : row("If it runs out", html `<p>${chain[0]}</p><ol class="approval-steps" data-approval-fallback>${chain.slice(1).map(one => html `<li>${one.replace(/^\d+\. /, "")}</li>`)}</ol>`),
|
|
455
474
|
html `</dl>`,
|
|
456
475
|
amendment,
|
|
@@ -460,8 +479,74 @@ export function approvalSheetParts(input) {
|
|
|
460
479
|
routeEvidenceHtml(route, "approval-evidence"),
|
|
461
480
|
consent,
|
|
462
481
|
]);
|
|
463
|
-
|
|
482
|
+
// The plan sheet (docs/plans/task-page-steps.md): the same parts, re-ordered and re-labelled to read top to bottom —
|
|
483
|
+
// one lead sentence, Steps, Done when, Will change / Won't touch, Worth knowing, the agent — with nothing a person
|
|
484
|
+
// signs inside a fold. Never a second reading of the scope: every term below is one of the parts above.
|
|
485
|
+
const parsedPlan = plan !== null && plan.ok ? plan.document : null;
|
|
486
|
+
const approach = parsedPlan?.approach.trim() ?? "";
|
|
487
|
+
const firstSentence = (text) => /^[\s\S]+?[.!?](?=\s|$)/.exec(text)?.[0] ?? text;
|
|
488
|
+
const sheetLead = approach !== "" ? firstSentence(approach) : goalShown.trim();
|
|
489
|
+
const approachRest = approach !== "" ? approach.slice(sheetLead.length).trim() : "";
|
|
490
|
+
const part = (title, body, attrs = html ``) => body === "" || isEmptyHtml(body) ? html `` : html `<section class="plan-sheet-part"${attrs}><h3 class="plan-sheet-h">${title}</h3>${body}</section>`;
|
|
491
|
+
const statementKey = (text) => text.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
492
|
+
const signedKeys = scope.acceptance.map(one => statementKey(one.statement));
|
|
493
|
+
// The plan's proof lines a requirement doesn't already say, word for word: a line that adds a condition to one stays.
|
|
494
|
+
const extraProof = (parsedPlan?.proof ?? []).filter(line => { const key = statementKey(line); return key !== "" && !signedKeys.includes(key); });
|
|
495
|
+
const doneWhenSheet = joinHtml([
|
|
496
|
+
scope.acceptance.length === 0 ? html `<p>You decide when you review the result.</p>` : html `<ul class="plan-sheet-done">${scope.acceptance.map(one => html `<li><span>${one.statement}</span><span class="plan-sheet-how">${one.evidence.map(kind => EVIDENCE_SHEET[kind] ?? kind).join(" · ")}</span></li>`)}</ul>`,
|
|
497
|
+
extraProof.length === 0 ? "" : html `<ul class="plan-sheet-done plan-sheet-proof">${extraProof.map(line => html `<li><span>${line}</span><span class="plan-sheet-how">From the plan</span></li>`)}</ul>`,
|
|
498
|
+
]);
|
|
499
|
+
const reportNote = input.deliverable === "report" ? html `<p class="approval-note">Read-only: it reports back and changes nothing in the repository.</p>` : "";
|
|
500
|
+
const notAllowingWarnings = allowing.filter(one => !warnings.includes(one));
|
|
501
|
+
const sheet = joinHtml([
|
|
502
|
+
html `<article class="plan-sheet" data-plan-sheet aria-label="${input.deliverable === "report" ? "The investigation" : "The plan"}">`,
|
|
503
|
+
sentence === null ? "" : html `<p class="approval-revision">${sentence}</p>`,
|
|
504
|
+
input.coordinator === null ? "" : html `<p class="approval-note">An agent filed this: <span class="mono">${input.coordinator.label}</span>${input.coordinator.filedAgo === null ? "" : html `, ${input.coordinator.filedAgo}`}. Nothing runs until you approve, and approving runs its request.</p>`,
|
|
505
|
+
reportNote,
|
|
506
|
+
originNote(input.origin) === null ? "" : html `<p class="approval-note" data-plan-origin>${originNote(input.origin)}</p>`,
|
|
507
|
+
html `<p class="${sheetLead.length > 180 ? "plan-sheet-lead plan-sheet-lead-long" : "plan-sheet-lead"}">${sheetLead}</p>`,
|
|
508
|
+
approachRest === "" ? "" : html `<p class="plan-sheet-text">${approachRest}</p>`,
|
|
509
|
+
// An older plan that doesn't parse keeps its text, as written.
|
|
510
|
+
plan !== null && !plan.ok ? html `<pre class="recap plan-doc">${input.planDocument ?? ""}</pre>` : "",
|
|
511
|
+
goalShown.trim() === sheetLead ? "" : part("Goal", html `<p class="approval-goal">${goalShown}</p>`),
|
|
512
|
+
repairText === null ? "" : part("Full goal", html `<p>${scope.goal}</p>`),
|
|
513
|
+
input.revision === null ? "" : part("Asked for changes", revisionBody),
|
|
514
|
+
releaseBody === "" ? "" : part("Release", releaseBody, html ` data-plan-release`),
|
|
515
|
+
milestones.length === 0 ? "" : part("Steps", html `<ol class="plan-sheet-steps">${milestones.map(one => html `<li><span>${one}</span></li>`)}</ol>`),
|
|
516
|
+
part("Done when", doneWhenSheet),
|
|
517
|
+
html `<div class="plan-sheet-pair">`,
|
|
518
|
+
part("Will change", scope.touches.length > 0 ? approvalPathsHtml(scope.touches) : html `<p>Any file in the project.</p>`),
|
|
519
|
+
part("Won’t touch", html `<p>${scope.outOfScope === null ? "Nothing is ruled out." : scope.outOfScope}</p>`),
|
|
520
|
+
html `</div>`,
|
|
521
|
+
part("Worth knowing", joinHtml([
|
|
522
|
+
amendment,
|
|
523
|
+
...warnings.map(one => html `<p class="plan-sheet-warn" data-plan-warning>${one.charAt(0).toUpperCase()}${one.slice(1)}.</p>`),
|
|
524
|
+
(parsedPlan?.risks.length ?? 0) === 0 ? "" : html `<ul class="plan-sheet-list">${parsedPlan.risks.map(one => html `<li>${one}</li>`)}</ul>`,
|
|
525
|
+
(parsedPlan?.dependencies.length ?? 0) === 0 ? "" : html `<p class="plan-sheet-sub">Depends on</p><ul class="plan-sheet-list">${parsedPlan.dependencies.map(one => html `<li>${one}</li>`)}</ul>`,
|
|
526
|
+
])),
|
|
527
|
+
part("The agent", joinHtml([
|
|
528
|
+
projection === null ? "" : sizeLineHtml(projection),
|
|
529
|
+
who.length === 0 ? "" : html `<p class="approval-who">${who.join(" · ")}</p>`,
|
|
530
|
+
routeEvidenceHtml(route, "approval-evidence"),
|
|
531
|
+
agentsDetail,
|
|
532
|
+
limits,
|
|
533
|
+
notAllowingWarnings.length === 0 ? "" : html `<p class="approval-allowing" data-plan-allowing>You’re allowing: ${notAllowingWarnings.join(" · ")}</p>`,
|
|
534
|
+
yoursToCheck.length === 0 ? "" : html `<p class="approval-you-check" data-plan-you-check>You’ll check: ${yoursToCheck.join("; ")}</p>`,
|
|
535
|
+
chain.length === 0 ? "" : html `<p>${chain[0]}</p><ol class="plan-sheet-list" data-plan-fallback>${chain.slice(1).map(one => html `<li>${one.replace(/^\d+\. /, "")}</li>`)}</ol>`,
|
|
536
|
+
])),
|
|
537
|
+
part("Plan record", contractDetail),
|
|
538
|
+
scope.candidate ? part("Saved commit", html `<p><code class="approval-commit">${scope.candidate}</code></p>`) : "",
|
|
539
|
+
part("Plan version", html `<p class="meta">Approving agrees to exactly this plan. <span class="seal mono" title="${planVersionWords(scope.digest)}">${planVersionWords(scope.digest)}</span></p>`),
|
|
540
|
+
html `</article>`,
|
|
541
|
+
]);
|
|
542
|
+
return { lead, rows, editor, editorForm, details, submit, label: rebuild !== null ? "Approve and retry" : "Approve", after, allowing, passwordNote, sheet, sheetLead };
|
|
543
|
+
}
|
|
544
|
+
/** The plan sheet: what an approval signs, read top to bottom (approvalSheetParts' own parts, re-ordered). */
|
|
545
|
+
export function planSheetHtml(input) {
|
|
546
|
+
return approvalSheetParts(input).sheet;
|
|
464
547
|
}
|
|
548
|
+
/** Evidence kinds on the plan sheet: how each requirement is proven, in plain words. */
|
|
549
|
+
export const EVIDENCE_SHEET = { check: "Runs automatically", screenshot: "Shown in a screenshot", "changed-path": "Seen in the changed files", "manual-review": "You review it" };
|
|
465
550
|
/** Earlier versions of a task still in flight, in plain words: queued
|
|
466
551
|
* while they only wait, running once one has started; null when none. */
|
|
467
552
|
export function earlierVersionsWords(active, running) {
|
|
@@ -835,6 +920,8 @@ export function approveRefusalWords(reason) {
|
|
|
835
920
|
return "not approved: only a person can approve this. Approve it on the task page or from your paired chat";
|
|
836
921
|
if (reason === "archived")
|
|
837
922
|
return "not approved: this task is archived. Restore it first";
|
|
923
|
+
if (reason === "cancelled")
|
|
924
|
+
return "not approved: this task was cancelled";
|
|
838
925
|
if (reason === "unrouted")
|
|
839
926
|
return "not approved: this plan was made before agents were chosen per task, so its old approval no longer stands. Edit and save the plan to choose today’s agents, then approve it";
|
|
840
927
|
if (reason === "profile-unresolved")
|
|
@@ -1216,7 +1303,7 @@ export function planRevisionLedgerHtml(ledger, taskId, csrf) {
|
|
|
1216
1303
|
html `</details>`,
|
|
1217
1304
|
]);
|
|
1218
1305
|
return joinHtml([
|
|
1219
|
-
html `<div class="card plan-revision"><span class="eyebrow">plan updated</span><h2>Using plan revision ${current.revision}</h2>`,
|
|
1306
|
+
html `<div class="card plan-revision" id="plan-revisions"><span class="eyebrow">plan updated</span><h2>Using plan revision ${current.revision}</h2>`,
|
|
1220
1307
|
html `<p class="meta">${current.reason}${current.evidenceLink === null ? "" : ` · evidence: ${current.evidenceLink}`}</p>`,
|
|
1221
1308
|
pending,
|
|
1222
1309
|
history,
|
|
@@ -1340,10 +1427,8 @@ export function taskBodyParts(data) {
|
|
|
1340
1427
|
if (data.peekable !== true)
|
|
1341
1428
|
return "";
|
|
1342
1429
|
const peek = html `<p class="meta">What is changing right now</p><div id="run-peek"><p class="meta">Watching… the first look lands within 15 seconds</p></div><p class="meta" id="run-peek-stamp"></p>`;
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
: html `<p class="meta">What the agent is saying</p><pre id="live-transcript" class="mono" style="max-height:18rem;overflow:auto;white-space:pre-wrap"></pre><p class="meta" id="live-transcript-state"></p>`;
|
|
1346
|
-
return html `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}${peek}${transcript}${door}</div>`;
|
|
1430
|
+
// What the agent is saying is the work panel's (Watch the work): the same records, read by the workspace.
|
|
1431
|
+
return html `<div class="card attempt-live" data-live-run="${liveRun.id}">${head}${peek}${door}</div>`;
|
|
1347
1432
|
})();
|
|
1348
1433
|
// One truthful answer to the first question on a queued task: "will this
|
|
1349
1434
|
// run?" The badge alone cannot distinguish approval, dependency,
|
|
@@ -1558,6 +1643,8 @@ export function taskBodyParts(data) {
|
|
|
1558
1643
|
// its draft; the full scope form below stays as it was.
|
|
1559
1644
|
const inlineScopeDraft = data.scopeDraft?.has("requirement-new") === true;
|
|
1560
1645
|
const scopeDraft = inlineScopeDraft ? undefined : data.scopeDraft;
|
|
1646
|
+
// A release's terms in the scope card (the page without script); the plan sheet says them whole (approvalSheetParts).
|
|
1647
|
+
const releaseTermsHtml = scope == null || (scope.releaseTerms == null && scope.releaseCheck == null) ? "" : html `<p><strong>Release</strong></p>${scope.releaseTerms == null ? "" : html `<p class="recap">${releaseTermsSentence(scope.releaseTerms)}</p>`}${scope.releaseCheck == null ? "" : html `<p class="recap">${releaseCheckSentence(scope.releaseCheck)}</p>`}${data.releaseCiRunning === true ? html `<p class="meta">${RELEASE_CI_RUNNING_WORDS}</p>` : ""}${scope.releaseTerms?.policy.carry === true ? html `<p><strong>What asks again</strong></p><ul>${joinHtml(releaseAsksAgainWords(scope.releaseTerms).map(line => html `<li>${line}</li>`), "")}</ul>` : ""}`;
|
|
1561
1648
|
const scopeCard = scope === null
|
|
1562
1649
|
? html `<p class="meta">No plan yet. Nothing builds until one is approved.</p>`
|
|
1563
1650
|
: joinHtml([
|
|
@@ -1567,7 +1654,7 @@ export function taskBodyParts(data) {
|
|
|
1567
1654
|
scope.touches.length === 0 ? "" : html `<p class="scope-paths"><strong>Files</strong> ${scope.touches.join(", ")}</p>`,
|
|
1568
1655
|
html `<p><strong>Quality</strong> ${qualityModeTitle(scope.qualityMode ?? "default")}</p>`,
|
|
1569
1656
|
acceptanceCeremonyHtml(scope.acceptance),
|
|
1570
|
-
|
|
1657
|
+
releaseTermsHtml,
|
|
1571
1658
|
approval.approved && data.autoRelease != null
|
|
1572
1659
|
? html `<p class="meta scope-seal">${autoReleaseWords(data.autoRelease)}</p>${planVersionDetailsHtml(scope.digest)}`
|
|
1573
1660
|
: approval.approved && data.release?.kind === "carried"
|
|
@@ -1629,6 +1716,14 @@ export function taskBodyParts(data) {
|
|
|
1629
1716
|
// card does not repeat it (one #contract-amendment on the page).
|
|
1630
1717
|
const sheetStatesContract = scope !== null && !approval.approved && data.plan === "drafted" && data.dispatch?.action !== "repair-dependency" &&
|
|
1631
1718
|
!(data.revision != null && "problem" in data.revision) && scope.profileState !== "unresolved" && consentDoorOf(scope, data.route).open;
|
|
1719
|
+
// The plan document's own editor (#plan-edit): the plan card holds it, and the task page's Plan row.
|
|
1720
|
+
const planEditorHtml = data.planDocument === null || data.csrf === "" || data.planSha == null || approval.approved
|
|
1721
|
+
? ""
|
|
1722
|
+
: html `<details class="plan-editor" id="plan-edit"><summary>Edit plan</summary>${postForm(`${taskHref(task.id)}/plan-edit`, joinHtml([
|
|
1723
|
+
html `<label>Plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>`,
|
|
1724
|
+
html `<textarea name="plan-document" rows="14">${data.planDocument}</textarea></label>`,
|
|
1725
|
+
html `<button type="submit">Save plan</button>`,
|
|
1726
|
+
]), { hidden: { "saw-plan": data.planSha } })}</details>`;
|
|
1632
1727
|
const planCard = data.planDocument === null
|
|
1633
1728
|
? data.plan === "requested"
|
|
1634
1729
|
// A request the planner cannot be given waits on a person; the status card above already says what to shorten.
|
|
@@ -1645,13 +1740,7 @@ export function taskBodyParts(data) {
|
|
|
1645
1740
|
html `</div><span class="plan-lock">${planStanding}</span>${approval.approved ? html `</summary><div class="planner-plan-body">` : html `</div>`}`,
|
|
1646
1741
|
executionPlanHtml(displayedPlanDocument ?? data.planDocument),
|
|
1647
1742
|
approval.approved || sheetStatesContract ? "" : planContractHtml(data.planContract ?? null, "full"),
|
|
1648
|
-
|
|
1649
|
-
? ""
|
|
1650
|
-
: html `<details class="plan-editor" id="plan-edit"><summary>Edit plan</summary>${postForm(`${taskHref(task.id)}/plan-edit`, joinHtml([
|
|
1651
|
-
html `<label>Plan details <span class="meta">Keep the five headings. Approval locks this version for the build.</span>`,
|
|
1652
|
-
html `<textarea name="plan-document" rows="14">${data.planDocument}</textarea></label>`,
|
|
1653
|
-
html `<button type="submit">Save plan</button>`,
|
|
1654
|
-
]), { hidden: { "saw-plan": data.planSha } })}</details>`,
|
|
1743
|
+
planEditorHtml,
|
|
1655
1744
|
approval.approved ? html `</div></details>` : html `</section>`,
|
|
1656
1745
|
]);
|
|
1657
1746
|
const progressCard = milestoneProgressHtml(data.milestoneProgress);
|
|
@@ -1687,7 +1776,8 @@ export function taskBodyParts(data) {
|
|
|
1687
1776
|
(data.assignment?.primaryAction?.code === "approve-scope" && data.assignment.primaryAction.target.taskId === task.id);
|
|
1688
1777
|
// The sheet's terms, when this person can approve them here: the task page's kit approval box (its Plan tab shows
|
|
1689
1778
|
// the rows) and the legacy sheet both read these.
|
|
1690
|
-
|
|
1779
|
+
// A cancelled task asks for no approval: nothing would start, so its page offers no Approve (reopening asks again).
|
|
1780
|
+
const sheetInput = scope === null || approval.approved || task.state === "cancelled" || data.plan === "requested" || (data.revision != null && "problem" in data.revision) ||
|
|
1691
1781
|
scope.profileState === "unresolved" || !consentDoor.open ? null : {
|
|
1692
1782
|
surface: "task",
|
|
1693
1783
|
action: `${taskHref(task.id)}/approve`,
|
|
@@ -1707,6 +1797,7 @@ export function taskBodyParts(data) {
|
|
|
1707
1797
|
coordinator: data.coordinator ?? null,
|
|
1708
1798
|
origin: data.origin ?? null,
|
|
1709
1799
|
deliverable: data.deliverable ?? "branch",
|
|
1800
|
+
releaseCiRunning: data.releaseCiRunning === true,
|
|
1710
1801
|
rebuild: data.assignment?.need != null && "key" in data.assignment.need && data.assignment.need.key === "replan" ? data.assignment.need.build : null,
|
|
1711
1802
|
editHref: data.csrf !== "" && data.planSha != null && data.planDocument !== null ? "#plan-edit" : "#scope",
|
|
1712
1803
|
notNowHref: "/tasks",
|
|
@@ -1716,7 +1807,7 @@ export function taskBodyParts(data) {
|
|
|
1716
1807
|
: { action: `${taskHref(task.id)}/scope`, draft: inlineScopeDraft ? data.scopeDraft ?? null : null, problem: inlineScopeDraft ? data.problem : null,
|
|
1717
1808
|
open: data.editPlan === true, stepsHref: data.planSha != null && data.planDocument !== null ? "#plan-edit" : null },
|
|
1718
1809
|
};
|
|
1719
|
-
const approveForm = scope === null || approval.approved || data.plan === "requested"
|
|
1810
|
+
const approveForm = scope === null || approval.approved || task.state === "cancelled" || data.plan === "requested"
|
|
1720
1811
|
? ""
|
|
1721
1812
|
: data.revision !== null && data.revision !== undefined && "problem" in data.revision
|
|
1722
1813
|
? html `<div class="card approve-form" id="approve"><p><strong>This task is waiting on you: approval is blocked.</strong></p><p class="meta">${data.revision.problem}. A revision can be approved only when its saved notes can be read.</p></div>`
|
|
@@ -2133,30 +2224,10 @@ export function taskBodyParts(data) {
|
|
|
2133
2224
|
: html `<details class="task-secondary-approval"><summary>Updated approval terms</summary>${approveForm}</details>`;
|
|
2134
2225
|
const optionsHtml = html `${identity}${dispatchStatus}${dependencyChoiceNeeded ? "" : actsBar}`;
|
|
2135
2226
|
const optionsOpen = data.assignment?.primaryAction?.code === "unhold" || data.assignment?.primaryAction?.code === "retry-task";
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
data.archived == null ? "" : html `<div class="task-archived" data-archived><p>${archivedWords(data.archived)} · ${whenUtc(data.archived.at)}</p>${data.canPutAway === true && data.csrf !== ""
|
|
2141
|
-
? postForm(`${taskHref(task.id)}/restore`, html `<button type="submit" class="secondary">Restore</button>`, { hidden: { return: taskHref(task.id) }, attrs: { class: "inline" } }) : ""}</div>`,
|
|
2142
|
-
(data.history ?? ""),
|
|
2143
|
-
// A put-away task's banner is its status: no Needs you card, no act, no approval form (each would be refused).
|
|
2144
|
-
data.archived == null ? statusHtml : "",
|
|
2145
|
-
controlHtml,
|
|
2146
|
-
previousResult === null ? "" : html `<details class="task-previous-result"><summary>${previousResult.title}</summary>${previousResult.html}</details>`,
|
|
2147
|
-
progressCard,
|
|
2148
|
-
revisionLedgerCard,
|
|
2149
|
-
planCard,
|
|
2150
|
-
mirrorCard,
|
|
2151
|
-
problemHtml,
|
|
2152
|
-
needsScopeCard,
|
|
2153
|
-
data.archived == null ? approvalHtml : "",
|
|
2154
|
-
html `<details class="task-status-details" id="task-diagnostics"${optionsOpen ? html ` open` : ""}><summary>Task options</summary>${optionsHtml}</details>`,
|
|
2155
|
-
// Evidence-first (M5.5): what needs you, then what happened — decisions
|
|
2156
|
-
// and incidents above the attempt ledger and spend, the mechanics
|
|
2157
|
-
// (scope, holds, acts) after. Only trustworthy facts moved up. The rail
|
|
2158
|
-
// (slice 1c) rides beside the main column on wide screens and above it
|
|
2159
|
-
// on narrow ones.
|
|
2227
|
+
// The Plan section after its scope card: the release, the recipe and audit links, the agents, the edit form.
|
|
2228
|
+
const scopeRest = [releaseCardHtml(task.id, data.release ?? null, data.canStopRelease === true), html `<p class="meta">${data.repo !== null && data.scope !== null ? html `<a href="/recipes/from-task?task=${encodeURIComponent(task.id)}">Reuse this plan as a recipe →</a> · ` : ""}<a href="${taskHref(data.rootId ?? task.id)}/evidence">Audit record →</a></p>`, agentsCardHtml(task.id, data.route, data.canEditRoute === true), revisionInApproval ? "" : revisionCard, data.completion != null ? "" : repairChainHtml(data.repairChain ?? null), scopeForm];
|
|
2229
|
+
// The page's sections, built once and in order (each `section` also records its part for the rebuilt page).
|
|
2230
|
+
const layoutTail = joinHtml([
|
|
2160
2231
|
html `<div class="task-layout"><div class="task-main">`,
|
|
2161
2232
|
attemptPanel,
|
|
2162
2233
|
data.coordinatorProposals == null || data.coordinatorProposals.rows.length === 0
|
|
@@ -2182,12 +2253,41 @@ export function taskBodyParts(data) {
|
|
|
2182
2253
|
section("plan",
|
|
2183
2254
|
// The recipe road rides with the scope it reuses (UI polish
|
|
2184
2255
|
// 2026-09-13), off the title-to-action path.
|
|
2185
|
-
joinHtml([html `<h2>Plan</h2>`, scopeCard,
|
|
2256
|
+
joinHtml([html `<h2>Plan</h2>`, scopeCard, ...scopeRest], "\n"), scopeDraft !== undefined || (data.plan !== "requested" && approveForm === "" && !(scope === null && canPlan)), undefined, "scope"),
|
|
2186
2257
|
dependencyChoiceNeeded ? "" : section("waits for", waitsForCard, (data.waitsFor ?? []).length > 0, (data.waitsFor ?? []).length),
|
|
2187
2258
|
section("holds", holds, true, data.holds.length),
|
|
2188
2259
|
cancelAct,
|
|
2189
2260
|
html `</div><aside class="task-rail">${rail}</aside></div>`,
|
|
2190
2261
|
], "\n");
|
|
2262
|
+
// `rows`: the task's step rows, every one open (the page without script, docs/plans/task-page-steps.md): the status,
|
|
2263
|
+
// the approval, the plan, the build's progress and the previous result move into their rows; everything else stays.
|
|
2264
|
+
const nativeBody = (rows) => joinHtml([
|
|
2265
|
+
// The title is bare; the receipt or shared task status leads once.
|
|
2266
|
+
html `<div class="task-title-row"><h1 class="task-main-title">${data.rootTitle ?? task.title}</h1>${data.csrf === "" ? "" : taskViewSwitch(data.rootId ?? task.id, "overview")}</div>`,
|
|
2267
|
+
data.versionLabel == null ? "" : html `<p class="meta">${data.versionLabel} · <a href="${taskHref(data.rootId ?? task.id)}">Current work</a></p>`,
|
|
2268
|
+
data.archived == null ? "" : html `<div class="task-archived" data-archived><p>${archivedWords(data.archived)} · ${whenUtc(data.archived.at)}</p>${data.canPutAway === true && data.csrf !== ""
|
|
2269
|
+
? postForm(`${taskHref(task.id)}/restore`, html `<button type="submit" class="secondary">Restore</button>`, { hidden: { return: taskHref(task.id) }, attrs: { class: "inline" } }) : ""}</div>`,
|
|
2270
|
+
(data.history ?? ""),
|
|
2271
|
+
rows ?? "",
|
|
2272
|
+
// A put-away task's banner is its status: no Needs you card, no act, no approval form (each would be refused).
|
|
2273
|
+
rows === null && data.archived == null ? statusHtml : "",
|
|
2274
|
+
rows === null ? controlHtml : "",
|
|
2275
|
+
rows !== null || previousResult === null ? "" : html `<details class="task-previous-result"><summary>${previousResult.title}</summary>${previousResult.html}</details>`,
|
|
2276
|
+
rows === null ? progressCard : "",
|
|
2277
|
+
rows === null ? revisionLedgerCard : "",
|
|
2278
|
+
rows === null ? planCard : "",
|
|
2279
|
+
mirrorCard,
|
|
2280
|
+
problemHtml,
|
|
2281
|
+
needsScopeCard,
|
|
2282
|
+
rows === null && data.archived == null ? approvalHtml : "",
|
|
2283
|
+
html `<details class="task-status-details" id="task-diagnostics"${optionsOpen ? html ` open` : ""}><summary>Task options</summary>${optionsHtml}</details>`,
|
|
2284
|
+
// Evidence-first (M5.5): what needs you, then what happened — decisions
|
|
2285
|
+
// and incidents above the attempt ledger and spend, the mechanics
|
|
2286
|
+
// (scope, holds, acts) after. Only trustworthy facts moved up. The rail
|
|
2287
|
+
// (slice 1c) rides beside the main column on wide screens and above it
|
|
2288
|
+
// on narrow ones.
|
|
2289
|
+
layoutTail,
|
|
2290
|
+
], "\n");
|
|
2191
2291
|
// Console v2: one thread in time order (the plan, the agent's notes and
|
|
2192
2292
|
// results, its questions, the person's replies) and the metadata grouped
|
|
2193
2293
|
// for a Details panel. Every form below keeps its own route and fields.
|
|
@@ -2244,13 +2344,19 @@ export function taskBodyParts(data) {
|
|
|
2244
2344
|
}
|
|
2245
2345
|
// What the agent is working through now, and how its plan changed.
|
|
2246
2346
|
const liveAt = liveRun?.startedAt ?? task.updatedAt;
|
|
2347
|
+
// The plan itself is the page's Plan row: the thread keeps one line saying it was written.
|
|
2247
2348
|
if (planCard !== "")
|
|
2248
2349
|
thread.push({ key: "plan", at: familyRuns.filter(run => run.role === "planner").map(run => run.finishedAt ?? run.startedAt).sort().at(-1) ?? task.updatedAt,
|
|
2249
|
-
kind: "plan", who: "agent", author: "", title: "
|
|
2350
|
+
kind: "plan", who: "agent", author: "", title: data.planDocument !== null ? "Plan written" : data.revision != null && !("problem" in data.revision) ? "Updating the plan" : "Planning", text: null, link: null, html: "", more: null });
|
|
2250
2351
|
if (!isEmptyHtml(progressCard))
|
|
2251
2352
|
thread.push({ key: "progress", at: liveAt, kind: "progress", who: "agent", author: "", title: "Progress", text: null, link: null, html: htmlString(progressCard), more: null });
|
|
2252
|
-
|
|
2253
|
-
|
|
2353
|
+
// A plan change is decided on the page (the Build row holds its card): the thread, which the work panel shows, keeps
|
|
2354
|
+
// one line and a link there. Nothing in the thread takes a decision.
|
|
2355
|
+
if (!isEmptyHtml(revisionLedgerCard)) {
|
|
2356
|
+
const waiting = data.planRevisions?.pending ?? null;
|
|
2357
|
+
thread.push({ key: "plan-revisions", at: liveAt, kind: "progress", who: "agent", author: "", title: waiting === null ? "Plan changes" : "Recommends a plan change",
|
|
2358
|
+
text: waiting === null ? null : oneLineOf(waiting.reason, 280), link: { label: waiting === null ? "See the plan changes" : "Decide on the task page", href: "#plan-revisions" }, html: "", more: null });
|
|
2359
|
+
}
|
|
2254
2360
|
// The change a person asked for: the revision's own brief.
|
|
2255
2361
|
if (data.revision != null && !("problem" in data.revision)) {
|
|
2256
2362
|
const feedback = data.revision;
|
|
@@ -2274,10 +2380,12 @@ export function taskBodyParts(data) {
|
|
|
2274
2380
|
text: [chosen, decision.note].filter((one) => one !== null && one !== "").join(" — ") || null, link: null, html: "", more: null });
|
|
2275
2381
|
}
|
|
2276
2382
|
}
|
|
2277
|
-
// Open questions
|
|
2383
|
+
// Open questions are answered on the page (their cards sit in the step that waits, at #task-questions): the thread
|
|
2384
|
+
// says what was asked and links there.
|
|
2278
2385
|
if (decisionRail !== "")
|
|
2279
2386
|
thread.push({ key: "questions", at: openDecisions.map(one => one.createdAt).sort()[0] ?? task.updatedAt, kind: "question", who: "agent", author: "",
|
|
2280
|
-
title: openDecisions.length === 1 ? "Asked you a question" : `Asked you ${openDecisions.length} questions`, text:
|
|
2387
|
+
title: openDecisions.length === 1 ? "Asked you a question" : `Asked you ${openDecisions.length} questions`, text: openDecisions.map(one => oneLineOf(one.question, 280)).join("\n"),
|
|
2388
|
+
link: { label: openDecisions.length === 1 ? "Answer it on the task page" : "Answer them on the task page", href: "#task-questions" }, html: "", more: null });
|
|
2281
2389
|
for (const note of data.steering ?? []) {
|
|
2282
2390
|
thread.push({ key: `steer-${note.id}`, at: note.createdAt, kind: "reply", who: "person", author: note.author, title: "Note to the agent", text: note.note, link: null, html: "", more: null });
|
|
2283
2391
|
}
|
|
@@ -2403,6 +2511,29 @@ export function taskBodyParts(data) {
|
|
|
2403
2511
|
{ group: "share", label: "Copy link", kind: "copy" },
|
|
2404
2512
|
...(cancelForm !== "" ? [{ group: "danger", label: "Cancel task", kind: "goto", tab: "plan", target: "cancel-task" }] : []),
|
|
2405
2513
|
];
|
|
2514
|
+
// The Plan row (docs/plans/task-page-steps.md): the plan sheet of the terms on file, built from the same parts as
|
|
2515
|
+
// the approval's (approvalSheetParts), and the plan's own editor. While the approval waits here the sheet is the
|
|
2516
|
+
// approval's own (`approve.planHtml`), so this row carries only the editor: the plan is shown once.
|
|
2517
|
+
const plannerRun = familyRuns.filter(run => run.role === "planner" && run.outcome !== null).sort((a, b) => b.id - a.id)[0];
|
|
2518
|
+
const sheetShown = scope === null ? null : sheetInput ?? {
|
|
2519
|
+
surface: "task", action: "", csrf: "", nonce: "", digest: scope.digest, returnTo: null, scope,
|
|
2520
|
+
planDocument: data.planDocument ?? null, planContract: data.planContract ?? null,
|
|
2521
|
+
revision: data.revision == null || "problem" in data.revision ? null : data.revision,
|
|
2522
|
+
revisionSourceHref: data.revision == null || "problem" in data.revision ? "" : `/r/${data.revision.sourceRun}`,
|
|
2523
|
+
repairChain: data.repairChain ?? null, route: data.route, coordinator: data.coordinator ?? null, origin: data.origin ?? null,
|
|
2524
|
+
deliverable: data.deliverable ?? "branch", releaseCiRunning: data.releaseCiRunning === true, editHref: "#scope", notNowHref: "/tasks", sticky: false,
|
|
2525
|
+
earlier: { active: data.assignment?.earlierActive ?? 0, running: data.assignment?.earlierRunning ?? 0 }, edit: null,
|
|
2526
|
+
};
|
|
2527
|
+
const sheetParts = sheetShown === null ? null : approvalSheetParts(sheetShown);
|
|
2528
|
+
const approvalWaitsHere = data.archived == null && sheetInput !== null && !dependencyChoiceNeeded && data.csrf !== "";
|
|
2529
|
+
// A revision's plan being updated with the notes: the planner's card says so above the plan it starts from.
|
|
2530
|
+
const planUpdating = data.planDocument === null && data.plan === "requested" && data.revision != null && !("problem" in data.revision) && planCard !== "";
|
|
2531
|
+
const planView = scope === null
|
|
2532
|
+
? (planCard === "" ? null : { lead: null, by: null, at: null, html: htmlString(planCard), planning: data.plan === "requested", updating: planUpdating, key: "card" })
|
|
2533
|
+
: { lead: oneLineOf(sheetParts.sheetLead, 160), by: plannerRun === undefined ? null : agentNameWords(plannerRun.provider, plannerRun.model),
|
|
2534
|
+
at: plannerRun?.finishedAt ?? scope.proposedAt ?? null, planning: planUpdating, updating: planUpdating,
|
|
2535
|
+
key: `${planUpdating ? "updating:" : ""}${approvalWaitsHere ? "editor" : `sheet:${scope.digest}`}`,
|
|
2536
|
+
html: htmlString(joinHtml([planUpdating ? planCard : "", approvalWaitsHere ? "" : sheetParts.sheet, planEditorHtml])) };
|
|
2406
2537
|
const view = {
|
|
2407
2538
|
kind: "task",
|
|
2408
2539
|
id: task.id,
|
|
@@ -2423,9 +2554,24 @@ export function taskBodyParts(data) {
|
|
|
2423
2554
|
failure: failedCard && data.failure != null ? { line: data.failure.line, evidence: data.failure.evidence, suggestion: data.failure.suggestion, link: data.failure.link } : null,
|
|
2424
2555
|
retry,
|
|
2425
2556
|
runChecks,
|
|
2426
|
-
progress,
|
|
2557
|
+
progress: progress === null ? null : { ...progress, steps: (data.milestoneProgress ?? []).map(one => ({ title: one.description, state: one.state })) },
|
|
2558
|
+
plan: planView,
|
|
2427
2559
|
// One quiet line under the step: what the agent did last, and when.
|
|
2428
2560
|
activity: liveRun === undefined ? null : data.activity ?? null,
|
|
2561
|
+
// The work panel's run: the live build, else this version's latest build or report. Its records are read through the
|
|
2562
|
+
// run page's own guarded window; a page asking for a password names no window at all.
|
|
2563
|
+
work: (() => {
|
|
2564
|
+
const recorded = (run) => run.role === "builder" || run.role === "scout";
|
|
2565
|
+
// Running: unfinished under the task's current claim (liveRun is the one run proved live; a builder whose repair
|
|
2566
|
+
// child is the live run shares that claim and is still this attempt).
|
|
2567
|
+
const running = (run) => liveRun !== undefined && run.outcome === null && run.finishedAt === null && run.leaseId === liveRun.leaseId;
|
|
2568
|
+
const builds = data.runs.filter(recorded).sort((a, b) => b.id - a.id);
|
|
2569
|
+
const run = builds.find(running) ?? builds[0];
|
|
2570
|
+
if (run === undefined)
|
|
2571
|
+
return null;
|
|
2572
|
+
const live = running(run);
|
|
2573
|
+
return { run: run.id, live, agent: agentNameWords(run.provider, run.model), transcript: degraded ? null : `/r/${run.id}?fragment=transcript&shape=records` };
|
|
2574
|
+
})(),
|
|
2429
2575
|
stop,
|
|
2430
2576
|
pause,
|
|
2431
2577
|
steering: data.csrf !== "" ? data.steerComposer ?? null : null,
|
|
@@ -2442,22 +2588,30 @@ export function taskBodyParts(data) {
|
|
|
2442
2588
|
recheckStopped: data.csrf !== "" && data.assignment?.primaryAction?.code === "recheck-stopped" && data.assignment.primaryAction.target.runId !== null
|
|
2443
2589
|
? { action: `${taskHref(data.assignment.rootId)}/recheck-stopped`, run: data.assignment.primaryAction.target.runId } : null,
|
|
2444
2590
|
rebuild,
|
|
2445
|
-
//
|
|
2591
|
+
// What needs a person on the page itself. Open questions and a plan change are answered here, in their step's row:
|
|
2592
|
+
// the thread (the work panel) only links to them.
|
|
2446
2593
|
lead: [
|
|
2447
2594
|
{ key: "history", html: data.history === undefined ? "" : htmlString(data.history) }, { key: "control", html: htmlString(stop === null ? controlHtml : taskControlDetailsHtml(data.control, task.id, data.csrf, "task", false, true)) }, { key: "problem", html: fragmentString(problemHtml) },
|
|
2595
|
+
{ key: "questions", html: decisionRail === "" ? "" : htmlString(decisionRail) }, { key: "plan-revisions", html: isEmptyHtml(revisionLedgerCard) ? "" : htmlString(revisionLedgerCard) },
|
|
2448
2596
|
{ key: "route-change", html: data.routeChange == null ? "" : htmlString(html `<div class="card"><p>${data.routeChange}</p></div>`) },
|
|
2449
2597
|
{ key: "fast-grown", html: data.fastGrown == null || data.csrf === "" ? "" : htmlString(html `<div class="card"><p>${data.fastGrown}</p>${postForm(`${taskHref(task.id)}/keep-grown`, btn("Approve it", { variant: "primary", type: "submit" }))}</div>`) },
|
|
2450
2598
|
{ key: "pull-request", html: fragmentString(pullRequestCard) },
|
|
2451
2599
|
{ key: "needs-scope", html: fragmentString(needsScopeCard) },
|
|
2452
2600
|
{ key: "mirror", html: fragmentString(mirrorCard) }, { key: "attempt", html: fragmentString(attemptPanel) },
|
|
2453
2601
|
].filter(one => one.html !== ""),
|
|
2454
|
-
// Open questions are
|
|
2602
|
+
// Open questions are a lead block (key "questions"), so they sit in the step that waits.
|
|
2455
2603
|
questions: "",
|
|
2456
2604
|
facts,
|
|
2457
2605
|
sections: [
|
|
2458
2606
|
...(previousResult === null || thread.some(one => one.more !== null) ? [] : [{ id: "result", title: previousResult.title, html: htmlString(previousResult.html), open: false, count: null }]),
|
|
2459
2607
|
// Questions are in the thread; the scope and the ledgers fold here until asked.
|
|
2460
|
-
|
|
2608
|
+
// The Plan and Approval rows hold the plan sheet (a release's terms included), so this page's Plan section keeps
|
|
2609
|
+
// only what the sheet leaves out (the links, the agents, the edit form): the plan is never printed a second time.
|
|
2610
|
+
...sectionParts.filter(one => !MANAGE.has(one.id) && one.id !== "decisions").map(one => one.id !== "scope" ? one : {
|
|
2611
|
+
...one,
|
|
2612
|
+
...(scope === null ? {} : { html: htmlString(joinHtml(scopeRest, "\n")) }),
|
|
2613
|
+
...(scopeDraft === undefined && (finished || approval.approved) ? { open: false } : {}),
|
|
2614
|
+
}),
|
|
2461
2615
|
],
|
|
2462
2616
|
manage: [
|
|
2463
2617
|
...sectionParts.filter(one => MANAGE.has(one.id)),
|
|
@@ -2483,7 +2637,22 @@ export function taskBodyParts(data) {
|
|
|
2483
2637
|
return { href: `/settings/flows?repo=${encodeURIComponent(data.repo)}&starter=${starter.id}#starter-${starter.id}`, starter: starter.name };
|
|
2484
2638
|
})(),
|
|
2485
2639
|
};
|
|
2486
|
-
|
|
2640
|
+
// Without script the steps are the page too: every row open, in order, each holding its own part.
|
|
2641
|
+
const rowFacts = data.archived != null ? null : stepRowsOf(view);
|
|
2642
|
+
const rowsHtml = rowFacts === null ? null : (() => {
|
|
2643
|
+
const here = rowFacts.find(row => row.state === "current" || row.state === "stuck") ?? rowFacts.find(row => row.open) ?? rowFacts.at(-1);
|
|
2644
|
+
const bodyOf = (key) => joinHtml([
|
|
2645
|
+
key === here.key ? statusHtml : "",
|
|
2646
|
+
key === "plan" ? planCard : "",
|
|
2647
|
+
// The shared approval sheet, its Plan details open here: nothing the yes agrees to is folded on this page.
|
|
2648
|
+
key === "approve" ? (approvalHtml !== "" ? approvalHtml : view.approved == null ? "" : html `<p class="meta">${view.approved.automatic ? `Approved automatically by ${view.approved.rule ?? "a rule"}` : `Approved by ${view.approved.by}`} · ${whenTime(view.approved.at)}</p>`) : "",
|
|
2649
|
+
key === "build" ? joinHtml([controlHtml, progressCard, revisionLedgerCard]) : "",
|
|
2650
|
+
key === "ready" && previousResult !== null ? html `<section class="task-previous-result" aria-label="${previousResult.title}">${previousResult.html}</section>` : "",
|
|
2651
|
+
]);
|
|
2652
|
+
return stepRows(rowFacts.map(row => ({ key: row.key, label: row.label, mark: row.mark, sentence: row.sentence, needsYou: row.needsYou, current: row.state === "current" || row.state === "stuck",
|
|
2653
|
+
at: row.at, when: row.at === null ? "" : whenText(row.at), body: bodyOf(row.key), open: true })));
|
|
2654
|
+
})();
|
|
2655
|
+
return { html: nativeBody(rowsHtml), view };
|
|
2487
2656
|
}
|
|
2488
2657
|
export function taskBody(data) {
|
|
2489
2658
|
return taskBodyParts(data).html;
|
|
@@ -2505,7 +2674,6 @@ export function taskPage(chrome, data) {
|
|
|
2505
2674
|
const liveRun = liveRunId === null ? undefined : data.runs.find(one => one.id === liveRunId);
|
|
2506
2675
|
const script = (liveRun === undefined ? "" : regionScript("check-progress", "check", `/r/${liveRun.id}`)) +
|
|
2507
2676
|
(liveRun !== undefined && data.peekable === true ? regionScript("run-peek", "peek", `/r/${liveRun.id}`) : "") +
|
|
2508
|
-
(liveRun !== undefined && data.peekable === true && liveRun.provider === "claude" ? transcriptScript(`/r/${liveRun.id}`) : "") +
|
|
2509
2677
|
(data.csrf !== "" && data.decisions.some(one => one.state === "open" || one.state === "expired") ? decisionAnswerScript() : "");
|
|
2510
2678
|
return screen(`task \u00b7 ${data.task.id}`, first.html, {
|
|
2511
2679
|
chrome,
|
|
@@ -2610,7 +2778,8 @@ export function approveKitOf(input, freshSince) {
|
|
|
2610
2778
|
rememberedUntil: until,
|
|
2611
2779
|
// Easy-start C5: the owner approving on this computer needs no password, and the line says so (not "Passwordless").
|
|
2612
2780
|
...(input.localOwner?.eligible === true ? { termsWords: "No password needed on this computer." } : {}),
|
|
2613
|
-
|
|
2781
|
+
// What it signs: the plan sheet (the same parts as the chat's sheet, re-ordered), then the in-place editor.
|
|
2782
|
+
planHtml: htmlString(joinHtml([parts.sheet, parts.editor ?? "", parts.editorForm])),
|
|
2614
2783
|
};
|
|
2615
2784
|
}
|
|
2616
2785
|
/** How an approved plan was approved, for the status box's terms line: by whom and how, or the rule that approved it. */
|
|
@@ -37,7 +37,7 @@ import { runActivityOf } from "../task-activity.js";
|
|
|
37
37
|
import { taskFingerprint } from "../task-live.js";
|
|
38
38
|
import { pullRequestFactOf } from '../task-status.js';
|
|
39
39
|
import { openWorkDecisionOf } from "../work-summary.js";
|
|
40
|
-
import { learningHtml } from "../workspace-ui.js";
|
|
40
|
+
import { learningHtml, stepRowsOf } from "../workspace-ui.js";
|
|
41
41
|
import { createHash, randomBytes } from "node:crypto";
|
|
42
42
|
import {} from "node:http";
|
|
43
43
|
import { agentChoicesFor, routeOfTask } from "../agentconfig.js";
|
|
@@ -661,6 +661,7 @@ export function createTaskViews(runtime) {
|
|
|
661
661
|
coordinator: view.coordinator ?? null,
|
|
662
662
|
origin: view.origin ?? null,
|
|
663
663
|
repairChain: view.repairChain ?? null,
|
|
664
|
+
releaseCiRunning: view.releaseCiRunning === true,
|
|
664
665
|
},
|
|
665
666
|
decisions: (view?.decisions ?? runtime.store.decisionsForTask(ref.id))
|
|
666
667
|
.filter(one => one.state === "open" || one.state === "expired")
|
|
@@ -886,6 +887,9 @@ export function createTaskViews(runtime) {
|
|
|
886
887
|
if (oneTaskView.release != null)
|
|
887
888
|
oneTaskView.journey = null;
|
|
888
889
|
}
|
|
890
|
+
// The step rows read the view as the page will show it (its result, its release, its checks words).
|
|
891
|
+
if (oneTaskView?.kind === "task")
|
|
892
|
+
oneTaskView.steps = stepRowsOf(oneTaskView);
|
|
889
893
|
// The Result tab's no-script page, under the task's own (the same HTML its result page serves), and its script.
|
|
890
894
|
if (extras.result != null) {
|
|
891
895
|
page.body = html `${page.body}<section class="task-result-tab" id="task-result" aria-label="Result">${extras.result.html}</section>`;
|
|
@@ -148,7 +148,7 @@ export function generalGroup(facts, flags) {
|
|
|
148
148
|
formRow("Theme", "Light, dark, or whatever this device uses.", postForm("/settings/appearance", html `<div class="k-seg" role="group" aria-label="Theme">${themeButton("system", "Match device")}${themeButton("light", "Light")}${themeButton("dark", "Dark")}</div>`, { returnTo: "/settings/general" })),
|
|
149
149
|
kitSectionForm({ proof: "appearance", action: "/settings/appearance", id: "accent", gate: true, hidden: { return: "/settings/general" },
|
|
150
150
|
body: joinHtml([
|
|
151
|
-
formRow("Accent", "Buttons
|
|
151
|
+
formRow("Accent", "Buttons, focus and what needs you.", select({ name: "accent", label: "Accent", current: facts.accent, items: [...ACCENT_CHOICES.map(one => [one.hex, one.id === "ink" ? "Ink (default)" : one.name]), ...(custom === null ? [] : [[custom, `Your own colour (${custom})`]])] })),
|
|
152
152
|
accentNoteWords === null ? "" : note(accentNoteWords),
|
|
153
153
|
]) }),
|
|
154
154
|
disclose("Use your own colour", kitSectionForm({ proof: "appearance", action: "/settings/appearance", id: "accent-own", gate: true, hidden: { return: "/settings/general" },
|