toolroll 0.9.86 → 0.9.87

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.
Files changed (171) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/agents-board.d.ts +12 -0
  3. package/dist/agents-board.js +26 -0
  4. package/dist/approval-rules-ui.d.ts +4 -1
  5. package/dist/approval-rules-ui.js +31 -15
  6. package/dist/assignment-ui.js +1 -1
  7. package/dist/backup-ui.d.ts +5 -4
  8. package/dist/backup-ui.js +65 -32
  9. package/dist/browser/terminal.css +1 -1
  10. package/dist/browser/terminal.js +57 -54
  11. package/dist/browser/workspace.css +2 -2
  12. package/dist/browser/workspace.js +35 -70
  13. package/dist/browser-crew.js +1 -1
  14. package/dist/browser-workspace.d.ts +329 -183
  15. package/dist/browser-workspace.js +10 -9
  16. package/dist/chat-controls.d.ts +1 -1
  17. package/dist/chat-controls.js +1 -1
  18. package/dist/chat-settings-page.d.ts +29 -9
  19. package/dist/chat-settings-page.js +59 -28
  20. package/dist/check-levels-ui.d.ts +2 -1
  21. package/dist/check-levels-ui.js +67 -58
  22. package/dist/contracts/cli.d.ts +43 -0
  23. package/dist/contracts/cli.js +4 -0
  24. package/dist/contracts/console-api.d.ts +1 -1
  25. package/dist/contracts/console-api.js +1 -1
  26. package/dist/credentials-ui.d.ts +5 -3
  27. package/dist/credentials-ui.js +58 -31
  28. package/dist/design/kit.css +685 -0
  29. package/dist/design/kit.d.ts +208 -0
  30. package/dist/design/kit.js +177 -0
  31. package/dist/design/tokens.css +19 -1
  32. package/dist/discord-settings.js +8 -4
  33. package/dist/export-ui.d.ts +6 -4
  34. package/dist/export-ui.js +18 -8
  35. package/dist/flow-changed.d.ts +14 -0
  36. package/dist/flow-changed.js +53 -0
  37. package/dist/flow-gallery-ui.d.ts +41 -4
  38. package/dist/flow-gallery-ui.js +95 -63
  39. package/dist/flow-starters-ui.d.ts +3 -9
  40. package/dist/flow-starters-ui.js +12 -23
  41. package/dist/flow-unattended-words.d.ts +20 -0
  42. package/dist/flow-unattended-words.js +24 -0
  43. package/dist/flow-unattended.d.ts +9 -5
  44. package/dist/flow-unattended.js +45 -23
  45. package/dist/flows-cli.js +7 -3
  46. package/dist/flows-ui.d.ts +12 -6
  47. package/dist/flows-ui.js +141 -30
  48. package/dist/integrations-ui.d.ts +6 -4
  49. package/dist/integrations-ui.js +34 -40
  50. package/dist/integrations.d.ts +2 -2
  51. package/dist/integrations.js +4 -4
  52. package/dist/kits-ui.d.ts +8 -10
  53. package/dist/kits-ui.js +29 -33
  54. package/dist/kits.js +1 -1
  55. package/dist/knowledge-ui.js +76 -19
  56. package/dist/ledger-view.d.ts +2 -0
  57. package/dist/ledger-view.js +41 -38
  58. package/dist/models-ui.d.ts +8 -3
  59. package/dist/models-ui.js +82 -59
  60. package/dist/monitoring-ui.d.ts +4 -2
  61. package/dist/monitoring-ui.js +44 -13
  62. package/dist/needs-you.d.ts +1 -1
  63. package/dist/needs-you.js +8 -0
  64. package/dist/operate-remote-arguments.js +1 -0
  65. package/dist/operate-remote.js +1 -1
  66. package/dist/operate.d.ts +1 -1
  67. package/dist/operate.js +38 -4
  68. package/dist/passwordless.d.ts +1 -1
  69. package/dist/passwordless.js +1 -1
  70. package/dist/people-audit-ui.d.ts +2 -1
  71. package/dist/people-audit-ui.js +25 -23
  72. package/dist/policy-ui.d.ts +3 -2
  73. package/dist/policy-ui.js +27 -21
  74. package/dist/policy.d.ts +3 -0
  75. package/dist/policy.js +14 -1
  76. package/dist/project-delete-ui.d.ts +4 -2
  77. package/dist/project-delete-ui.js +135 -126
  78. package/dist/project-knowledge.d.ts +10 -10
  79. package/dist/publish.d.ts +11 -0
  80. package/dist/publish.js +16 -3
  81. package/dist/pull-request-ui.d.ts +2 -1
  82. package/dist/pull-request-ui.js +39 -28
  83. package/dist/recipe-ui.d.ts +4 -2
  84. package/dist/recipe-ui.js +14 -4
  85. package/dist/release-adapters.d.ts +3 -0
  86. package/dist/release-adapters.js +22 -2
  87. package/dist/release-cli.js +81 -9
  88. package/dist/release.d.ts +21 -0
  89. package/dist/release.js +40 -4
  90. package/dist/releases.js +1 -1
  91. package/dist/request-budget-ui.d.ts +4 -2
  92. package/dist/request-budget-ui.js +30 -19
  93. package/dist/retention-ui.d.ts +4 -3
  94. package/dist/retention-ui.js +15 -12
  95. package/dist/scope.d.ts +3 -0
  96. package/dist/scope.js +6 -0
  97. package/dist/serve.d.ts +0 -1
  98. package/dist/serve.js +51 -7
  99. package/dist/server/agents-computers.d.ts +9 -0
  100. package/dist/server/agents-computers.js +67 -0
  101. package/dist/server/chat.js +16 -16
  102. package/dist/server/chrome.d.ts +1 -1
  103. package/dist/server/chrome.js +19 -9
  104. package/dist/server/console.css +2 -2
  105. package/dist/server/flows.js +64 -36
  106. package/dist/server/home.d.ts +35 -0
  107. package/dist/server/home.js +104 -0
  108. package/dist/server/http.d.ts +3 -0
  109. package/dist/server/http.js +5 -0
  110. package/dist/server/kit-standalone.d.ts +22 -0
  111. package/dist/server/kit-standalone.js +69 -0
  112. package/dist/server/kit.d.ts +405 -0
  113. package/dist/server/kit.js +361 -0
  114. package/dist/server/pages.d.ts +2 -1
  115. package/dist/server/pages.js +213 -174
  116. package/dist/server/people-tokens.js +26 -85
  117. package/dist/server/render-history.d.ts +51 -0
  118. package/dist/server/render-history.js +228 -0
  119. package/dist/server/render-new-task.d.ts +29 -0
  120. package/dist/server/render-new-task.js +96 -0
  121. package/dist/server/render-next.d.ts +63 -0
  122. package/dist/server/render-next.js +109 -0
  123. package/dist/server/render-pages.d.ts +22 -190
  124. package/dist/server/render-pages.js +123 -575
  125. package/dist/server/render-people.d.ts +24 -3
  126. package/dist/server/render-people.js +93 -72
  127. package/dist/server/render-projects.d.ts +103 -0
  128. package/dist/server/render-projects.js +247 -0
  129. package/dist/server/render-results.d.ts +5 -24
  130. package/dist/server/render-results.js +48 -185
  131. package/dist/server/render-settings.d.ts +5 -74
  132. package/dist/server/render-settings.js +102 -493
  133. package/dist/server/render-tasks.d.ts +35 -5
  134. package/dist/server/render-tasks.js +114 -85
  135. package/dist/server/route-table.js +20 -5
  136. package/dist/server/settings.js +276 -255
  137. package/dist/server/task-views.d.ts +8 -0
  138. package/dist/server/task-views.js +85 -2
  139. package/dist/server/tasks.d.ts +8 -0
  140. package/dist/server/tasks.js +95 -40
  141. package/dist/server/terminals.js +1 -1
  142. package/dist/server/welcome.d.ts +22 -0
  143. package/dist/server/welcome.js +69 -0
  144. package/dist/settings-groups-ui.d.ts +279 -0
  145. package/dist/settings-groups-ui.js +615 -0
  146. package/dist/settings-kit.d.ts +42 -6
  147. package/dist/settings-kit.js +69 -7
  148. package/dist/setup-guide.d.ts +2 -2
  149. package/dist/skills-ui.js +59 -7
  150. package/dist/slack-settings.js +12 -5
  151. package/dist/spend-ui.d.ts +45 -26
  152. package/dist/spend-ui.js +124 -56
  153. package/dist/sso-ui.d.ts +5 -2
  154. package/dist/sso-ui.js +63 -31
  155. package/dist/storage-ui.d.ts +3 -2
  156. package/dist/storage-ui.js +56 -50
  157. package/dist/store.d.ts +112 -3
  158. package/dist/store.js +292 -20
  159. package/dist/surface.js +5 -1
  160. package/dist/teams-settings.d.ts +2 -1
  161. package/dist/teams-settings.js +24 -41
  162. package/dist/telegram-settings.d.ts +0 -4
  163. package/dist/telegram-settings.js +52 -25
  164. package/dist/toolroll-update-ui.d.ts +8 -4
  165. package/dist/toolroll-update-ui.js +55 -45
  166. package/dist/tools-ui.d.ts +5 -3
  167. package/dist/tools-ui.js +107 -42
  168. package/dist/work-index.js +11 -7
  169. package/dist/workspace-ui.d.ts +1 -1
  170. package/dist/workspace-ui.js +7 -4
  171. package/package.json +1 -1
@@ -1,37 +1,32 @@
1
- import { activityMetrics, activityScope, activityLimit } from '../project-activity.js';
2
1
  /** Everyday console pages: home, inbox, board, work, projects, fleet and the queue. */
3
2
  import { archivedWords, tidyGroups, TIDY_DEFAULT_DAYS } from "../tidy.js";
4
3
  import { stepUpForm } from "../settings-kit.js";
5
- import { originNote } from "../task-source.js";
6
- import { html, htmlString, joinHtml, postForm, replaceMarkup, reauth } from "../html.js";
4
+ import { html, joinHtml, postForm, replaceMarkup, reauth } from "../html.js";
7
5
  import {} from "../assignment.js";
8
6
  import {} from "../board.js";
9
7
  import { browserWorkActionHref } from "../browser-workspace.js";
10
- import { CHECK_LEVEL_WORDS } from "../check-levels.js";
11
8
  import {} from "../dispatch.js";
12
9
  import {} from "../first-run.js";
13
10
  import {} from "../gaps.js";
14
11
  import { limitsHtml } from "../limits-ui.js";
15
12
  import { ASK_LABEL, ASKS, START_BUILDER_COMMAND } from "../needs-you.js";
16
- import {} from "../onboard.js";
17
13
  import {} from "../peek.js";
18
14
  import { projectName } from "../project.js";
19
15
  import {} from "../proof.js";
20
- import {} from "../quality.js";
21
16
  import { isAlive as runnerAlive } from "../runner.js";
22
17
  import { agentModelName, modelName } from "../model-names.js";
23
- import {} from "../scope.js";
24
18
  import {} from "../store.js";
25
19
  import { runCostWords, spendLine, tally } from "../summary.js";
26
20
  import {} from "../work-index.js";
21
+ import { HEADLINES } from "../task-status.js";
22
+ import { chipOfHeadline } from "../design/kit.js";
27
23
  import { WORK_VIEWS } from "../workspace-ui.js";
28
24
  import { createHash } from "node:crypto";
29
- import { homedir } from "node:os";
30
25
  import { join } from "node:path";
31
- import { buildsViews, CHEVRON_ICON, consoleSpend, FOLDER_PATHS, GRIP_HANDLE, isOverdue, projectChip, relativeAge, safePrUrl, screen, sentenceCase, strokeIcon, TO_FRONT_ICON, when, whenTime } from "./chrome.js";
26
+ import { buildsViews, CHEVRON_ICON, consoleSpend, GRIP_HANDLE, isOverdue, projectChip, relativeAge, safePrUrl, screen, sentenceCase, TO_FRONT_ICON, when, whenTime } from "./chrome.js";
32
27
  import { reviewHref, taskHref } from "./http.js";
33
28
  import { criterionMatrixSummary, incidentWords, PHASE_WORDS, phaseWords } from "./render-results.js";
34
- import { acceptanceCeremonyHtml, agentsCeremonyHtml, consentClosedHtml, consentDoorOf, decisionAnswerCard, decisionAnswerScript, decisionOptionForms, dispatchHeadline, executionPlanHtml, permissionModeChoices, planContractHtml, profileWords, runtimeDetailsHtml, statusLineHtml, workDiagnosticsHtml } from "./render-tasks.js";
29
+ import { decisionAnswerCard, decisionAnswerScript, dispatchHeadline, statusLineHtml, workDiagnosticsHtml } from "./render-tasks.js";
35
30
  export const TASK_STATES = ["queued", "running", "done", "failed", "cancelled"];
36
31
  export const INBOX_TABS = [
37
32
  { id: "needs-you", label: "Needs you" }, { id: "ready", label: "Ready" }, { id: "running", label: "Running" }, { id: "all", label: "All" },
@@ -150,9 +145,8 @@ export function inboxPage(chrome, data) {
150
145
  : {}),
151
146
  });
152
147
  }
153
- /** System: the machinery — workers, background service, workspaces. */
154
148
  /** Start a builder: the one command, as text to select (/start-builder, where every "Connect a builder" leads, and
155
- * /system#builder). `project`: the open project's name, when the viewer may see it; `connected`: whether a builder
149
+ * the Agents page). `project`: the open project's name, when the viewer may see it; `connected`: whether a builder
156
150
  * that may build there is checking in (null: say nothing); `managed`: whether this person can start one themselves
157
151
  * (registering a builder takes an instance operator), else who to ask. */
158
152
  export function startBuilderHtml(data) {
@@ -165,65 +159,6 @@ export function startBuilderHtml(data) {
165
159
  ? html `<p class="meta">Keep it running. Approved tasks start on their own. The console alone (<code>toolroll serve</code>) doesn't build.</p>`
166
160
  : html `<p class="meta">Starting a builder takes someone who manages this Toolroll. Ask them to run it there; your tasks start on their own once it connects.</p>`}</div></section>`;
167
161
  }
168
- export function systemPage(chrome, data) {
169
- const nowMs = data.now.getTime();
170
- const runnerCards = data.runners
171
- .filter(one => one.retiredAt === null)
172
- .map(one => {
173
- const age = nowMs - new Date(one.heartbeatAt).getTime();
174
- const dot = age < 5 * 60_000 ? "dot-ok" : age < 60 * 60_000 ? "dot-warn" : "dot-off";
175
- const said = age < 5 * 60_000 ? "alive" : age < 60 * 60_000 ? `quiet ${Math.round(age / 60_000)}m` : "not heard from";
176
- const busy = data.building.filter(claim => claim.runner === one.name).length;
177
- return html `<div class="stat-card"><span class="k"><span class="dot ${dot}"></span>${one.name}</span><span class="v">builder · ${said} · ${busy}/${one.capacity} building</span></div>`;
178
- });
179
- const worktreeCards = data.worktrees.map(tree => {
180
- const leased = tree.leasedAt !== null && tree.releasedAt === null;
181
- const dot = leased ? "dot-ok" : tree.verified ? "dot-off" : "dot-warn";
182
- const state = leased ? "building" : tree.verified ? "free" : "needs review";
183
- const name = tree.path.split("/").pop() ?? tree.path;
184
- return html `<div class="stat-card"><span class="k"><span class="dot ${dot}${leased ? " pulse" : ""}"></span><span class="mono">${name}</span></span><span class="v">workspace · ${tree.branch} · ${state}</span></div>`;
185
- });
186
- const watchCard = data.episode === null
187
- ? null
188
- : html `<div class="stat-card"><span class="k"><span class="dot ${data.episode.endedAt === null ? "dot-ok pulse" : "dot-off"}"></span>Toolroll</span><span class="v">${data.episode.endedAt === null
189
- ? html `running since ${whenTime(data.episode.startedAt)}`
190
- : html `last run: ${data.episode.built} built, ${data.episode.broke} broke · ended ${whenTime(data.episode.endedAt)}`}</span></div>`;
191
- const cards = [...runnerCards, watchCard, ...worktreeCards].filter((one) => one !== null);
192
- const PHASE_SAID = {
193
- plan: "planning sessions ask questions and draft the plan you approve",
194
- build: "builds do the work, unattended, inside the approved scope",
195
- repair: "repair turns mend a malformed handoff in the same session",
196
- };
197
- const agentLines = joinHtml(data.agents
198
- .map(one => {
199
- if (one.problem !== undefined) {
200
- return html `<p class="row"><span class="mono">${one.phase}</span> <span class="badge badge-failed">Misconfigured</span> <span class="meta">${one.problem}</span></p>`;
201
- }
202
- const who = one.source === "project"
203
- ? html `chosen for this project${one.setBy === null ? null : html ` by ${one.setBy}`}`
204
- : one.source === "installation"
205
- ? html `set for the whole installation${one.setBy === null ? null : html ` by ${one.setBy}`}`
206
- : html `the default — nothing configured`;
207
- const dollars = one.provider === "claude" ? null : html ` · <span title="this provider reports tokens, not dollars — its runs land as unmeasured spend">no dollar costs</span>`;
208
- return html `<p class="row"><span class="mono">${one.phase}</span> <strong>${one.provider === null || one.provider === undefined ? "" : agentModelName(one.provider, one.model ?? null)}</strong>${one.model === null || one.model === undefined ? html ` <span class="meta">(its default model)</span>` : null}<span class="right meta">${who}${dollars}</span></p><p class="meta" style="margin-top:0">${PHASE_SAID[one.phase] ?? ""}</p>`;
209
- }), "\n");
210
- const agentsCard = html `<h2>Agents</h2><p class="meta">Which AI provider runs each phase. An instance operator changes them in <a href="/settings/models">Settings → Models</a> with their password, or with <code>toolroll config</code>.</p><div class="card">${agentLines}<p class="meta">Repair always stays on the provider that built — only its model can differ. A schedule's task filed under an approval moved from a routine is pinned to the agents approved then.</p></div>`;
211
- return screen("system", joinHtml([
212
- html `<h1>System</h1>`,
213
- html `<p class="hint">builders execute tasks in isolated temporary copies of each project</p>`,
214
- agentsCard,
215
- cards.length === 0 ? null : html `<div class="cards">${cards}</div>`,
216
- startBuilderHtml({ heading: "h2", project: null, connected: data.runners.some(one => one.retiredAt === null && runnerAlive(one, data.now)) ? null : false, managed: true }),
217
- data.outboxPending > 0 ? html `<p class="meta">Notifications: ${data.outboxPending} pending delivery</p>` : null,
218
- (data.externalWork ?? []).length === 0
219
- ? null
220
- : html `<h2>External work</h2>${joinHtml((data.externalWork ?? []).map(one => html `<p class="row"><span class="mono">${one.remoteRepo}</span> ${one.blocked !== null
221
- ? html `<span class="badge badge-failed">Dispatch blocked</span> <span class="meta">the tracker connection needs repair — \`toolroll sync\` says why</span>`
222
- : one.openEpisode !== null
223
- ? html `<span class="badge badge-failed">Sync failing</span> <span class="meta">${one.openEpisode}</span>`
224
- : html `<span class="meta">syncing normally</span>`}</p>`), "\n")}`,
225
- ], "\n"), { chrome, refreshSeconds: data.building.length > 0 ? 10 : 60 });
226
- }
227
162
  /**
228
163
  * A build's stage as one of three steps — set up, writing, checking and
229
164
  * committing — read from the machine's own phase, never a percent. The
@@ -449,28 +384,6 @@ export function boardBody(data, ciRed) {
449
384
  html `</div>`,
450
385
  ], "\n");
451
386
  }
452
- /** Completed work: one row per done task, its final run and PR attached. */
453
- export function donePage(chrome, rows, ciRed) {
454
- const list = rows.length === 0
455
- ? html `<p class="meta">No finished tasks yet.</p>`
456
- : joinHtml(rows.map(row => {
457
- const pr = row.prNumber === null
458
- ? row.publicationState === null
459
- ? null
460
- : html ` <span class="badge">${sentenceCase(row.publicationState)}</span>`
461
- : html ` <a href="${row.prUrl ?? "#"}" class="badge badge-open">PR #${row.prNumber}</a>${ciRed(row.prNumber) ? html ` <span class="badge badge-failed">CI failing</span>` : null}`;
462
- const needsVerification = (row.proofVerdict === "short" || row.proofVerdict === "refuted") && !row.proofAccepted
463
- ? html ` <span class="badge badge-failed">${row.proofVerdict === "refuted" ? "conflicting evidence" : "missing evidence"}</span>`
464
- : null;
465
- return html `<div class="card"><p><a href="${taskHref(row.taskId)}"><strong>${row.title}</strong></a>${row.outcome === "no-change" ? html ` <span class="badge">No change needed</span>` : null}${pr}${needsVerification}${criterionMatrixSummary(row.proofMatrix)}</p>${row.handoff === null ? null : html `<p class="meta">${row.handoff.length > 200 ? row.handoff.slice(0, 200) + "…" : row.handoff}</p>`}<p class="meta mono">${row.taskId} · ${whenTime(row.completedAt)}${row.ranMinutes === null ? null : html ` · ran ${row.ranMinutes}m`}${row.provider === null ? null : html ` · ${runCostWords({ authMode: row.authMode, costUsd: row.costUsd, tokensIn: null, tokensOut: null })}`} · <a href="${reviewHref(row.taskId)}">review →</a></p></div>`;
466
- }), "\n");
467
- return screen("done", joinHtml([
468
- html `<h1>Done</h1>`,
469
- buildsViews("done"),
470
- html `<p class="hint">completed work in order of completion — each with its final build, the agent's conclusion, usage, and its pull request; <a href="/review">review</a> opens the same saved work and its checks</p>`,
471
- list,
472
- ], "\n"), { chrome });
473
- }
474
387
  export function homePage(chrome, data) {
475
388
  const { summary } = data;
476
389
  // The night's harvest is the page's reason to exist — strong figures in a
@@ -553,91 +466,132 @@ export function homePage(chrome, data) {
553
466
  fleet,
554
467
  ], "\n"), { chrome, refreshSeconds: refresh });
555
468
  }
556
- /** The one front door for new work. The common path is one prompt and one
557
- * button; the detailed contract remains available in-place for templates,
558
- * experts, and the rare task that should skip repository-aware planning. */
559
- /** Where new work goes when no project is open: a choice from the projects
560
- * this person already has, never a typed path; a new project is one link
561
- * away. Same-named checkouts show their parent folder to tell them apart. */
562
- export function projectPickerHtml(projects, chosen) {
563
- if (projects.length === 0) {
564
- return html `<label class="task-repo">Project folder <span class="meta">— no project is open, so the task must say where it belongs</span><input type="text" name="repo" value="${chosen}" required placeholder="/path/to/repository"></label><p class="meta task-repo-add">No projects yet. <a href="/projects?return=%2Ftasks%2Fnew">Add a project</a> to pick it here next time.</p>`;
469
+ export const TASK_GROUP_LABEL = { ...ASK_LABEL, building: 'Building', rest: 'Recent' };
470
+ /** A list row's chip (src/design/KIT.md): a waiting row names its ask ("Plan", "Result") in the ask tone, or says
471
+ * Needs you; every other row wears its headline in the tone of the status it belongs to. */
472
+ export function workRowChip(row) {
473
+ if (row.archived !== undefined)
474
+ return { word: 'Archived', tone: '' };
475
+ if (row.ask !== null)
476
+ return row.chip === null ? { word: 'Needs you', tone: 'needs' } : { word: row.chip, tone: 'ask' };
477
+ const label = row.status.label;
478
+ if (HEADLINES.includes(label)) {
479
+ const chip = chipOfHeadline(label);
480
+ return { word: chip.word, tone: chip.tone };
565
481
  }
566
- const counts = new Map();
567
- for (const one of projects)
568
- counts.set(one.name, (counts.get(one.name) ?? 0) + 1);
569
- const label = (one) => {
570
- if ((counts.get(one.name) ?? 0) < 2)
571
- return one.name;
572
- const parent = one.path.split(/[\\/]/).filter(Boolean).slice(-2, -1)[0] ?? one.path;
573
- return `${one.name} (${parent})`;
574
- };
575
- const selected = projects.some(one => one.path === chosen) ? chosen : projects[0].path;
576
- return html `<label class="task-repo">Project<select name="repo" required>${projects.map(one => html `<option value="${one.path}" title="${one.path}"${one.path === selected ? html ` selected` : null}>${label(one)}</option>`)}</select></label><p class="meta task-repo-add"><a href="/projects?return=%2Ftasks%2Fnew">Add a project</a></p>`;
482
+ const tone = row.status.tone;
483
+ return { word: label, tone: tone === 'live' ? 'live' : tone === 'done' ? 'done' : tone === 'problem' ? 'failed' : '' };
577
484
  }
578
- export function taskComposerHtml(data) {
579
- const prefill = data.prefill ?? null;
580
- const values = prefill?.values;
581
- const after = values?.get("after") ?? "";
582
- const candidates = data.candidates ?? (after === "" ? [] : [{ id: after, title: after }]);
583
- const projectLabel = data.project === null ? "repository required" : projectName(data.project);
584
- const showPicker = data.project === null || (data.projects !== undefined && data.projects.length > 1);
585
- const quality = values?.get("quality-mode") ?? data.qualityDefault;
586
- return postForm("/tasks/add", joinHtml([
587
- prefill === null || values !== undefined
588
- ? null
589
- : html `<p class="meta" style="margin:.35rem .75rem .15rem">pre-filled from a template. Change anything; it still waits for your approval.</p>`,
590
- html `<label class="task-prompt"><span class="visually-hidden">What should get done?</span><textarea name="title" rows="4" maxlength="200" required autofocus placeholder="Describe the outcome you want. The planner will inspect the repository and work out the implementation details.">${prefill === null ? "" : prefill.title}</textarea></label>`,
591
- // The project is always a visible, changeable choice when the page knows
592
- // the person's projects; the open project is simply preselected.
593
- showPicker ? projectPickerHtml(data.projects ?? [], values?.get("repo") ?? data.project ?? "") : null,
594
- html `<div class="task-composer-footer">`,
595
- html `<div class="task-context">${showPicker || data.project === null ? null : html `<span class="task-context-chip" title="${data.project}">${projectLabel}</span>`}<span class="task-context-chip">planner inspects first</span></div>`,
596
- html `<label class="task-quality"><span class="visually-hidden">quality mode</span><select name="quality-mode" aria-label="quality mode"><option value="default"${quality === "default" ? html ` selected` : null}>Default quality</option><option value="strict"${quality === "strict" ? html ` selected` : null}>Strict / release</option></select></label>`,
597
- html `<button type="submit" class="task-submit">${prefill === null ? "Plan task" : "Continue"} →</button>`,
598
- html `</div>`,
599
- html `<details class="task-options"${prefill === null ? null : html ` open`}>`,
600
- html `<summary><span>Edit details</span><small>optional · defaults are remembered</small></summary>`,
601
- html `<div class="task-options-grid">`,
602
- html `<label class="wide">Goal <span class="meta">— provide upfront for automatic approval of an unchanged plan</span><textarea name="goal" rows="3" placeholder="What success looks like">${prefill === null ? "" : prefill.goal}</textarea></label>`,
603
- html `<label class="wide">Acceptance <span class="meta">— required with a goal; one per line: <code>statement | evidence,kinds | how</code></span><textarea name="acceptance" rows="3" placeholder="Requests over the limit return 429 | check">${prefill === null ? "" : prefill.acceptance}</textarea></label>`,
604
- html `<label>Not this <span class="meta">— optional boundary</span><input type="text" name="not" value="${prefill === null ? "" : prefill.not}"></label>`,
605
- html `<label>Likely touches <span class="meta">— paths, comma-separated</span><input type="text" name="touches" value="${prefill === null ? "" : prefill.touches}"></label>`,
606
- html `<label class="wide task-check"><input type="checkbox" name="plan-first" value="1"${values === undefined || values.get("plan-first") === "1" ? html ` checked` : null}><span><strong>Let the planner inspect first</strong><small class="meta">Recommended. It drafts the goal, acceptance criteria, and implementation approach, and asks only when a missing answer materially changes the work.</small></span></label>`,
607
- html `<label class="wide task-check"><input type="checkbox" name="scout" value="1"${values?.get("scout") === "1" ? html ` checked` : null}><span><strong>Research only</strong><small class="meta">Deliver a read-only report instead of changing the repository.</small></span></label>`,
608
- html `<label>Task id <span class="meta">— optional</span><input type="text" name="id" value="${values?.get("id") ?? ""}" placeholder="made from the request"></label>`,
609
- candidates.length === 0
610
- ? null
611
- : html `<label>Starts after <span class="meta">— optional</span><select name="after"><option value="">right away</option>${candidates.map(one => html `<option value="${one.id}"${one.id === after ? html ` selected` : null}>${one.id} — ${one.title}</option>`)}</select></label>`,
612
- html `<fieldset class="permission-field"><legend>Agent permissions</legend>${permissionModeChoices("permission-mode", values?.get("permission-mode") === "bypassPermissions" ? "bypassPermissions" : values?.get("permission-mode") === "auto" ? "auto" : data.permissionDefault)}<p class="meta permission-note">Inherited from Settings. You can still change it on the proposed scope before approval.</p></fieldset>`,
613
- html `</div>`,
614
- html `</details>`,
615
- ], "\n"), {
616
- attrs: { class: "card task-composer" },
617
- projectRevision: data.projectRevision === undefined && values?.get("projectRevision") == null ? null : String(data.projectRevision ?? values?.get("projectRevision") ?? ""),
618
- hidden: { "planning-policy": "choice" },
619
- });
485
+ /** Where a row's one action goes: a result opens on its task's Result tab (the review page is the task page now). */
486
+ function workActionHref(row) {
487
+ const target = row.primaryAction?.target;
488
+ if (row.primaryAction?.code === 'open-result' && target?.runId != null)
489
+ return `${taskHref(target.taskId)}?tab=result&run=${target.runId}`;
490
+ return browserWorkActionHref(row);
620
491
  }
621
- export const TASK_GROUP_LABEL = { ...ASK_LABEL, building: 'Building', rest: 'Recent' };
622
492
  export function workPage(chrome, data) {
623
- // The stored-state filter (the old task list's) rides every link but a change of state, which starts on page one.
624
- const href = (view, cursor, state = data.state ?? null) => {
625
- const query = new URLSearchParams();
626
- if (data.projectFilter !== undefined)
627
- query.set('project', data.projectFilter);
493
+ const layout = data.board != null ? 'board' : 'list';
494
+ // The project and the stored-state filter ride every link; a change of filter starts on page one.
495
+ const query = (over = {}) => {
496
+ const q = new URLSearchParams();
497
+ const project = over.project === undefined ? (data.projectFilter ?? (data.all === true ? 'all' : null)) : over.project;
498
+ if (project === 'all')
499
+ q.set('all', '1');
500
+ else if (project !== null)
501
+ q.set('project', project);
502
+ const view = over.view ?? data.view;
628
503
  if (view !== 'all')
629
- query.set('view', view);
504
+ q.set('view', view);
505
+ const state = over.state === undefined ? data.state ?? null : over.state;
630
506
  if (state !== null)
631
- query.set('state', state);
632
- if (cursor !== undefined)
633
- query.set('cursor', cursor);
634
- return `/work${query.size ? `?${query}` : ''}`;
507
+ q.set('state', state);
508
+ if (over.cursor !== undefined)
509
+ q.set('cursor', over.cursor);
510
+ if ((over.layout ?? layout) === 'board')
511
+ q.set('layout', 'board');
512
+ return `/work${q.size ? `?${q}` : ''}`;
635
513
  };
636
- const tabs = html `<nav class="work-views" aria-label="Task views">${WORK_VIEWS.map(one => html `<a href="${href(one.key)}"${one.key === data.view ? html ` class="active" aria-current="page"` : null}>${one.label}<span class="count">${data.work.totals[one.key]}</span></a>`)}</nav>`;
514
+ const href = (view, cursor, state = data.state ?? null) => query({ view, ...(cursor === undefined ? {} : { cursor }), state });
637
515
  // Put away (or Restore, in Archived): one quiet act per row, never on a task with work going on.
638
516
  const tidyAct = (row) => data.canPutAway !== true ? null : row.archived !== undefined ? { action: `${taskHref(row.rootId)}/restore`, label: 'Restore' }
639
517
  : row.liveRunId !== null || row.unfinishedRunId !== null || row.status.views.includes('running') ? null : { action: `${taskHref(row.rootId)}/archive`, label: 'Put away' };
640
518
  const returnTo = href(data.view, undefined);
519
+ const current = WORK_VIEWS.find(one => one.key === data.view);
520
+ const filtered = (data.state ?? null) !== null;
521
+ const projectOf = (row) => data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null;
522
+ // The status filters, as addresses, each with its count; Archived sits at the foot of Finished, not in the track.
523
+ const tabs = WORK_VIEWS.filter(one => one.key !== 'archived').map(one => ({ key: one.key, label: one.label, href: href(one.key), count: data.work.totals[one.key], active: one.key === data.view }));
524
+ // The project filter: All and each project in view, when there is more than one.
525
+ const known = chrome.projects ?? [];
526
+ const lens = data.lens === undefined ? chrome.project : data.lens;
527
+ const projects = known.length < 2 ? null : [
528
+ { key: '', label: 'All', href: query({ project: 'all' }), active: lens === null },
529
+ ...known.map(one => ({ key: one.path, label: one.name, href: query({ project: one.path }), active: lens === one.path })),
530
+ ];
531
+ const rowOf = (row) => {
532
+ const act = tidyAct(row);
533
+ const tidy = act === null ? null : { action: act.action, label: act.label, returnTo };
534
+ const chip = workRowChip(row);
535
+ if (row.archived !== undefined)
536
+ return {
537
+ id: row.rootId, title: row.title, href: taskHref(row.rootId), project: projectOf(row), age: relativeAge(row.archived.at, data.now),
538
+ status: { label: 'Archived', tone: 'muted', token: 'archived' }, chip: null, word: chip,
539
+ ask: null, group: 'rest', rank: 4, action: null, detail: archivedWords(row.archived), problem: null, notes: [], tidy,
540
+ };
541
+ const actionHref = workActionHref(row);
542
+ const needsYouDetail = (row.status.views.includes('needs-you') || row.status.label === 'Failed') && !['write-scope', 'approve-scope'].includes(row.primaryAction?.code ?? '') && row.status.label !== 'Ready for review' && row.status.detail !== row.status.label && row.status.detail !== row.familyProblem;
543
+ return {
544
+ id: row.rootId, title: row.title, href: taskHref(row.rootId), project: projectOf(row), age: relativeAge(row.updatedAt, data.now),
545
+ status: { label: row.status.label, tone: row.status.tone, token: row.status.token }, chip: row.chip, word: chip,
546
+ ask: row.ask, group: row.ask ?? (row.status.rank === 1 ? 'building' : 'rest'), rank: row.status.rank,
547
+ // One quiet action, on a row that waits on you; every other row is its own link.
548
+ action: actionHref === null || row.primaryAction === null || row.ask === null ? null : { label: row.primaryAction.label, href: actionHref },
549
+ detail: needsYouDetail ? row.status.detail : row.progress ?? (row.ask === null && row.status.detail !== row.status.label ? row.status.detail : null),
550
+ problem: row.familyProblem,
551
+ notes: (row.status.diagnostics ?? []).map(one => `${one.label} · ${one.detail}`),
552
+ tidy,
553
+ };
554
+ };
555
+ const rows = data.work.items.map(rowOf);
556
+ const board = data.board == null ? null : [
557
+ ['needs', 'Needs you', data.board.needs, 'needs-you', 'Nothing needs you.'],
558
+ ['queued', 'Queued', data.board.queued, 'queued', 'Nothing queued.'],
559
+ ['running', 'Running', data.board.running, 'running', 'Nothing running.'],
560
+ ['finished', 'Finished', data.board.finished, 'completed', 'Nothing has finished yet.'],
561
+ ].map(([key, label, page, view, empty]) => ({ key, label, count: page.totals[view], href: query({ view, layout: 'list' }), empty, rows: page.items.map(rowOf) }));
562
+ const putAwayLink = data.canPutAway === true && data.view !== 'archived' ? { label: 'Put away old tasks', href: '/work/put-away' } : null;
563
+ const toolLinks = [['/board?view=order', 'Order the queue'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']]), ['/ledger', 'Action ledger']];
564
+ const newTaskHref = lens === null ? '/tasks/new' : `/tasks/new?project=${encodeURIComponent(lens)}`;
565
+ const emptyNow = data.work.items.length > 0 ? null : filtered && !data.previous ? { text: `No tasks are ${data.state} in this view.`, action: { label: 'Show every state', href: href(data.view, undefined, null) } } : {
566
+ text: data.previous ? 'There are no more tasks on this page.' : current.empty,
567
+ action: data.view === 'all' && !data.previous ? { label: chrome.chat === true ? 'Start in chat' : 'Add a task', href: chrome.chat === true ? '/chat' : newTaskHref } : data.view === 'all' ? null : { label: 'Show all tasks', href: href('all') },
568
+ };
569
+ const view = {
570
+ kind: 'tasks',
571
+ layout,
572
+ tabs,
573
+ projects,
574
+ layouts: { list: query({ layout: 'list' }), board: query({ layout: 'board', view: 'all' }) },
575
+ scope: lens === null ? 'across all your projects' : `in ${known.find(one => one.path === lens)?.name ?? projectName(lens)}`,
576
+ needsYou: data.work.totals['needs-you'],
577
+ groups: data.view !== 'all' && data.view !== 'needs-you' ? null
578
+ : [...ASKS, ...(data.view === 'all' ? ['building', 'rest'] : [])]
579
+ .map(key => ({ key, label: TASK_GROUP_LABEL[key], count: data.work.groups[key] })).filter(one => one.count > 0),
580
+ rows,
581
+ board,
582
+ archived: data.view === 'completed' || data.view === 'archived' ? { label: 'Archived', href: href('archived'), count: data.work.totals.archived, active: data.view === 'archived' } : null,
583
+ empty: emptyNow,
584
+ pages: { first: data.previous ? href(data.view) : null, next: data.work.nextCursor === null ? null : href(data.view, data.work.nextCursor) },
585
+ tools: toolLinks.map(([path, label]) => ({ label: label, href: path })),
586
+ newTask: { label: 'New task', href: newTaskHref },
587
+ putAway: putAwayLink,
588
+ limits: data.limits ?? null,
589
+ states: [{ label: 'Any state', href: href(data.view, undefined, null), active: (data.state ?? null) === null },
590
+ ...TASK_STATES.map(one => ({ label: one.charAt(0).toUpperCase() + one.slice(1), href: href(data.view, undefined, one), active: data.state === one }))],
591
+ };
592
+ // The page without the workspace (and its first paint) keeps the older rows the server tests read; the workspace
593
+ // replaces it with the kit view above.
594
+ const legacyTabs = html `<nav class="work-views" aria-label="Task views">${WORK_VIEWS.map(one => html `<a href="${href(one.key)}"${one.key === data.view ? html ` class="active" aria-current="page"` : null}>${one.label}<span class="count">${data.work.totals[one.key]}</span></a>`)}</nav>`;
641
595
  const rowHtml = (row) => {
642
596
  if (row.archived !== undefined) {
643
597
  const act = tidyAct(row);
@@ -654,8 +608,6 @@ export function workPage(chrome, data) {
654
608
  const putAway = act === null ? null : postForm(act.action, html `<button type="submit" class="quiet">${act.label}</button>`, { hidden: { return: returnTo }, attrs: { class: 'inline', 'data-tidy-act': 'put-away' } });
655
609
  return html `<article class="work-row" data-task="${row.rootId}" data-work-status="${row.status.token}" data-work-views="${row.status.views.join(' ')}"><div class="work-row-main"><a class="work-title" href="${taskHref(row.rootId)}">${row.title}</a><p class="work-meta">${project}<span>${relativeAge(row.updatedAt, data.now)}</span><span class="mono">${row.rootId}</span></p></div><div class="work-row-status">${statusLineHtml(row.status)}${action}${putAway}${row.familyProblem === null ? null : html `<p class="problem">${row.familyProblem}</p>`}${row.status.views.includes('needs-you') && !['write-scope', 'approve-scope'].includes(row.primaryAction?.code ?? '') && row.status.detail !== row.status.label && row.status.detail !== row.familyProblem ? html `<p class="work-detail">${row.status.detail}</p>` : null}${workDiagnosticsHtml(row.status.diagnostics)}</div></article>`;
656
610
  };
657
- const current = WORK_VIEWS.find(one => one.key === data.view);
658
- const filtered = (data.state ?? null) !== null;
659
611
  // The state filter, as real links (the old task list's), so a page without the React view can still change it.
660
612
  const stateLinks = html `<nav class="work-states" aria-label="Task state"><span class="meta">State:</span> ${joinHtml([html `<a href="${href(data.view, undefined, null)}"${filtered ? null : html ` aria-current="page"`}>Any</a>`,
661
613
  ...TASK_STATES.map(one => html `<a href="${href(data.view, undefined, one)}"${data.state === one ? html ` aria-current="page"` : null}>${one}</a>`)], html ` · `)}</nav>`;
@@ -665,77 +617,9 @@ export function workPage(chrome, data) {
665
617
  ? html `<div class="work-empty" data-work-empty="${data.view}"><p>${data.previous ? 'There are no more tasks on this page.' : current.empty}</p>${data.view === 'all' && !data.previous ? html `<p><a class="button-link" href="${chrome.chat === true ? '/chat' : '/tasks/new'}">${chrome.chat === true ? 'Start in chat' : 'Add a task'}</a></p>` : html `<a href="${href('all')}">See all tasks →</a>`}</div>`
666
618
  : html `<div class="work-list">${data.work.items.map(rowHtml)}</div>`;
667
619
  const pages = !data.previous && data.work.nextCursor === null ? null : html `<nav class="row work-pagination" aria-label="Task pages">${data.previous ? html `<a class="button-link" href="${href(data.view)}">First page</a>` : null}${data.work.nextCursor === null ? null : html `<a class="button-link" rel="next" href="${href(data.view, data.work.nextCursor)}">Next page</a>`}</nav>`;
668
- const tools = html `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">${[['/inbox', 'Inbox'], ['/board', 'Board'], ['/board?view=order', 'Order'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']]), ['/ledger', 'Action ledger']]
620
+ const tools = html `<details class="work-tools"><summary>Work tools${CHEVRON_ICON}</summary><nav class="work-tools-menu">${[['/inbox', 'Inbox'], ['/work?layout=board', 'Board'], ['/board?view=order', 'Order'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']]), ['/ledger', 'Action ledger']]
669
621
  .map(([path, label]) => html `<a href="${path}">${label}</a>`)}</nav></details>`;
670
- const putAwayLink = data.canPutAway === true && data.view !== 'archived' ? { label: 'Put away old tasks', href: '/work/put-away' } : null;
671
- const toolLinks = [['/inbox', 'Inbox'], ['/board', 'Board'], ['/board?view=order', 'Order'], ['/recipes', 'Recipes'], ...(chrome.projectScoped ? [] : [['/workbench', 'Portfolio']])];
672
- const view = {
673
- kind: 'tasks',
674
- tabs: WORK_VIEWS.map(one => ({ label: one.label, href: href(one.key), count: data.work.totals[one.key], active: one.key === data.view })),
675
- needsYou: data.work.totals['needs-you'],
676
- groups: data.view !== 'all' && data.view !== 'needs-you' ? null
677
- : [...ASKS, ...(data.view === 'all' ? ['building', 'rest'] : [])]
678
- .map(key => ({ key, label: TASK_GROUP_LABEL[key], count: data.work.groups[key] })).filter(one => one.count > 0),
679
- rows: data.work.items.map(row => {
680
- const target = row.primaryAction?.target;
681
- const actionHref = row.primaryAction?.code === 'open-result' && target?.runId != null
682
- ? `/review?result=${encodeURIComponent(target.taskId)}&run=${target.runId}`
683
- : browserWorkActionHref(row);
684
- const act = tidyAct(row);
685
- const tidy = act === null ? null : { action: act.action, label: act.label, returnTo };
686
- if (row.archived !== undefined)
687
- return {
688
- id: row.rootId, title: row.title, href: taskHref(row.rootId),
689
- project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
690
- age: relativeAge(row.archived.at, data.now), status: { label: 'Archived', tone: 'muted', token: 'archived' },
691
- ask: null, chip: null, group: 'rest', action: null, detail: archivedWords(row.archived), problem: null, notes: [], tidy,
692
- };
693
- const needsYouDetail = (row.status.views.includes('needs-you') || row.status.label === 'Failed') && !['write-scope', 'approve-scope'].includes(row.primaryAction?.code ?? '') && row.status.label !== 'Ready for review' && row.status.detail !== row.status.label && row.status.detail !== row.familyProblem;
694
- return {
695
- id: row.rootId, title: row.title, href: taskHref(row.rootId),
696
- project: data.multiProject || row.repo === null ? (row.repo === null ? 'Unplaced' : projectName(row.repo)) : null,
697
- age: relativeAge(row.updatedAt, data.now),
698
- status: { label: row.status.label, tone: row.status.tone, token: row.status.token },
699
- ask: row.ask, chip: row.chip, group: row.ask ?? (row.status.rank === 1 ? 'building' : 'rest'),
700
- action: actionHref === null || row.primaryAction === null ? null : { label: row.primaryAction.label, href: actionHref },
701
- detail: needsYouDetail ? row.status.detail : row.progress ?? null,
702
- problem: row.familyProblem,
703
- notes: (row.status.diagnostics ?? []).map(one => `${one.label} · ${one.detail}`),
704
- tidy,
705
- };
706
- }),
707
- empty: data.work.items.length > 0 ? null : filtered && !data.previous ? { text: `No tasks are ${data.state} in this view.`, action: { label: "Show every state", href: href(data.view, undefined, null) } } : {
708
- text: data.previous ? 'There are no more tasks on this page.' : current.empty,
709
- action: data.view === 'all' && !data.previous ? { label: chrome.chat === true ? 'Start in chat' : 'Add a task', href: chrome.chat === true ? '/chat' : '/tasks/new' } : { label: 'See all tasks', href: href('all') },
710
- },
711
- pages: { first: data.previous ? href(data.view) : null, next: data.work.nextCursor === null ? null : href(data.view, data.work.nextCursor) },
712
- tools: toolLinks.map(([path, label]) => ({ label: label, href: path })),
713
- newTask: { label: 'New task', href: '/tasks/new' },
714
- putAway: putAwayLink,
715
- limits: data.limits ?? null,
716
- states: [{ label: 'Any state', href: href(data.view, undefined, null), active: (data.state ?? null) === null },
717
- ...TASK_STATES.map(one => ({ label: one.charAt(0).toUpperCase() + one.slice(1), href: href(data.view, undefined, one), active: data.state === one }))],
718
- };
719
- return screen('work', html `<div class="work-head"><h1>Tasks</h1>${putAwayLink === null ? null : html `<a class="button-link" href="${putAwayLink.href}" data-put-away-old>${putAwayLink.label}</a>`}${tools}</div>${limitsHtml(data.limits ?? null)}${tabs}${stateLinks}${list}${pages}`, { chrome, workspace: { view } });
720
- }
721
- export function browsePage(chrome, data) {
722
- const crumb = data.at === data.root ? projectName(data.root) : `${projectName(data.root)}${data.at.slice(data.root.length)}`;
723
- const openForm = (path) => postForm("/projects/open", html `<button type="submit">Open</button>`, { attrs: { class: "inline" }, hidden: { path } });
724
- return screen("projects", joinHtml([
725
- html `<h1>Choose a folder</h1>`,
726
- html `<p class="meta">git repositories float to the top and can be opened; anything else can be entered — only folders under ${data.roots.length === 1 ? html `<span class="mono">${projectName(data.root)}</span>` : "the configured roots"} are visible here</p>`,
727
- data.roots.length > 1
728
- ? html `<p class="meta">Roots: ${joinHtml(data.roots.map(one => html `<a href="/projects/browse?at=${encodeURIComponent(one)}" class="mono">${projectName(one)}</a>`), " · ")}</p>`
729
- : null,
730
- html `<p class="mono meta">${crumb}</p>`,
731
- data.parent === null
732
- ? null
733
- : html `<p class="row"><a href="/projects/browse?at=${encodeURIComponent(data.parent)}">← up one level</a></p>`,
734
- data.entries.length === 0
735
- ? html `<p class="meta">No folders here</p>`
736
- : joinHtml(data.entries.map(one => html `<p class="row"><a href="/projects/browse?at=${encodeURIComponent(one.path)}"><strong>${one.name}</strong></a>${one.git ? html ` <span class="badge badge-done">git</span>` : null}<span class="right">${one.git ? openForm(one.path) : html `<a class="meta" href="/projects/browse?at=${encodeURIComponent(one.path)}">enter →</a>`}</span></p>`), "\n"),
737
- html `<p class="meta"><a href="/projects">← back to projects</a></p>`,
738
- ], "\n"), { chrome });
622
+ return screen('work', html `<div class="work-head"><h1>Tasks</h1>${putAwayLink === null ? null : html `<a class="button-link" href="${putAwayLink.href}" data-put-away-old>${putAwayLink.label}</a>`}${tools}</div>${limitsHtml(data.limits ?? null)}${legacyTabs}${stateLinks}${list}${pages}`, { chrome, workspace: { view } });
739
623
  }
740
624
  /**
741
625
  * The control-room rail (attended A1): every state that matters while a
@@ -905,188 +789,6 @@ export function githubIdentityOf(remoteUrl) {
905
789
  return `${ssh[1]}/${ssh[2]}`;
906
790
  return null;
907
791
  }
908
- /** The GitHub listing page: the account's repositories with ONE honest
909
- * action each. Names, descriptions, and paths are gh/filesystem DATA —
910
- * escaped at the sink like everything else. */
911
- export function githubReposPage(chrome, data) {
912
- const openForm = (path, label) => postForm("/projects/open", html `<button type="submit">${label}</button>`, { attrs: { class: "inline" }, hidden: { path } });
913
- const cloneForm = (nameWithOwner) => postForm("/projects/onboard-preview", html `<button type="submit">Clone here →</button>`, { attrs: { class: "inline" }, hidden: { repo: nameWithOwner, root: "0" } });
914
- const list = (label) => postForm("/projects/github", html `<button type="submit"${label === "Refresh list" ? html ` class="secondary"` : ""}>${label}</button>`, { attrs: { class: "inline" } });
915
- const rows = data.listed === null
916
- ? list("List repositories")
917
- : !data.listed.ok
918
- ? html `<p class="meta">${data.listed.message}</p>`
919
- : data.listed.repos.length === 0
920
- ? html `<p class="meta">The signed-in GitHub account has no repositories to list</p>`
921
- : joinHtml(data.listed.repos.map(repo => {
922
- const localPath = data.local.get(repo.nameWithOwner.toLowerCase()) ?? null;
923
- const action = localPath !== null && data.openProject === localPath
924
- ? html `<span class="badge badge-done">Open now</span>`
925
- : localPath !== null
926
- ? openForm(localPath, data.registered.has(localPath) ? "open →" : "add + open →")
927
- : data.cloneReady
928
- ? cloneForm(repo.nameWithOwner)
929
- : html `<span class="meta">choose a projects folder first</span>`;
930
- return joinHtml([
931
- html `<div class="card project-card">`,
932
- html `<div class="row"><strong>${repo.nameWithOwner}</strong>${repo.isPrivate ? html ` <span class="badge">Private</span>` : null}`,
933
- html `<span class="right">${action}</span></div>`,
934
- localPath === null
935
- ? html `<p class="meta">Not on this machine yet${/^\d{4}-\d{2}-\d{2}T/.test(repo.updatedAt) ? html ` · pushed ${whenTime(repo.updatedAt)}` : null}</p>`
936
- : html `<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${localPath}</p>`,
937
- repo.description === "" ? null : html `<p class="meta">${repo.description}</p>`,
938
- html `</div>`,
939
- ], "\n");
940
- }), "\n");
941
- return screen("projects", joinHtml([
942
- html `<h1>Your GitHub repositories</h1>`,
943
- html `<p class="meta">Repositories available to the GitHub account signed in on this machine — open one you already have, or clone a new one after a quick preview.</p>`,
944
- data.localUnanswered === true ? html `<p class="meta">Your projects folder didn't answer, so copies already on this machine aren't matched yet. Try again in a moment.</p>` : null,
945
- rows,
946
- data.listed === null ? null : list("Refresh list"),
947
- html `<p class="row" style="margin-top:.6rem"><a class="badge" href="/projects">← back to projects</a></p>`,
948
- ], "\n"), { chrome });
949
- }
950
- export function projectsPage(chrome, recent, candidates, open, problem, _unscopedMode, browsable = false, onboard = null, peeks = {}, returnTo = "/", pullRequestsOn, checkLevelOf, activity) {
951
- const activityHtml = activity === undefined ? null : html `<details><summary>Activity · ${activity.project}</summary>${activity.report === null
952
- ? html `<p role="alert">Activity is unavailable.</p><a href="/projects">Reload</a>`
953
- : html `<p>${activityScope(activity.report)}</p>${activity.report.truncated ? html `<p>${activityLimit(activity.report)}</p>` : null}${activityMetrics(activity.report).map(metric => html `<details><summary>${metric.label} · ${metric.value}</summary><p>${metric.detail}</p>${metric.rows.length === 0 ? html `<p>No tasks in this group.</p>` : html `<ul>${metric.rows.map(row => html `<li><a href="${row.href}">${row.title}</a> · Recorded checks: ${row.checks}</li>`)}</ul>`}</details>`)}`}</details>`;
954
- // The onboarding card (repo onboarding, findings 1-39): preview first,
955
- // then a password-confirmed clone into a configured root. Disabled
956
- // states explain themselves in words (finding 28/39).
957
- const onboardCard = onboard === null
958
- ? null
959
- : !onboard.enabled
960
- ? html `<p class="meta">${onboard.why}</p>`
961
- : joinHtml([
962
- html `<details class="project-add-more"${onboard.record === null ? null : html ` open`}><summary>${onboard.record === null ? 'Paste a GitHub link' : 'Review repository'}</summary>`,
963
- html `<p class="meta">Paste a GitHub repository — you will preview it before anything is downloaded. It goes into your saved projects folder and the builder connects automatically. Large-file (LFS) objects are not downloaded.</p>`,
964
- onboard.record === null
965
- ? postForm("/projects/onboard-preview", joinHtml([
966
- html `<label>Repository <input type="text" name="repo" placeholder="owner/name or https://github.com/owner/name"></label>`,
967
- onboard.roots.length > 1
968
- ? html `<label>Into <select name="root">${onboard.roots.map((one, index) => html `<option value="${index}">${one}</option>`)}</select></label>`
969
- : html `<input type="hidden" name="root" value="0"><p class="meta">Into ${onboard.roots[0] ?? ""}</p>`,
970
- html `<button type="submit">Preview</button>`,
971
- ], "\n"), { attrs: { class: "card" } })
972
- : joinHtml([
973
- html `<div class="card">`,
974
- html `<p><strong>${onboard.record[1].nameWithOwner}</strong> <span class="meta">${onboard.record[1].diskUsageKib === null ? "size unknown" : `${Math.max(1, Math.round(onboard.record[1].diskUsageKib / 1024))} MiB`} — will land at ${onboard.record[1].target}</span></p>`,
975
- postForm("/projects/onboard-confirm", joinHtml([
976
- onboard.record[1].large
977
- ? html `<label class="row"><input type="checkbox" name="big-ok" value="1"> this is a large repository (or its size is unknown) — clone it anyway</label>`
978
- : null,
979
- html `${reauth(html `<label>Your password, typed again <input type="password" name="token" autocomplete="current-password"></label>`)}`,
980
- html `<div class="sticky-actions"><button type="submit">Clone and open</button></div>`,
981
- ], "\n"), { hidden: { nonce: onboard.record[0] } }),
982
- html `</div>`,
983
- ], "\n"),
984
- html `</details>`,
985
- ], "\n");
986
- // Opening a project is a POST (the session's scope changes); a card's
987
- // name and counts are the same form, returning to the screen that count
988
- // names — so every number on this page is a road, not a fact to admire.
989
- const openForm = (path, label, destination = returnTo, className) => postForm("/projects/open", html `<button type="submit"${className === undefined ? null : html ` class="${className}"`}${className === "button-link" ? html ` style="min-height:44px"` : null}>${label}</button>`, { attrs: { class: "inline" }, returnTo: destination, hidden: { path } });
990
- // A project switcher CARD (v30 UI): the name and path, an at-a-glance
991
- // peek — what waits on a person, what is queued or running, what built
992
- // in the last day — and the open action. A vertical stack that reads on
993
- // a phone, not a dense row.
994
- const peekChips = (path, peek) => {
995
- if (peek === null)
996
- return html `<span class="meta">not scanned</span>`;
997
- const isOpen = open !== null && open === path;
998
- const chip = (text, href, cls) => isOpen ? html `<a class="badge ${cls}" href="${href}">${text}</a>` : openForm(path, text, href, `badge ${cls}`);
999
- const bits = [];
1000
- if (peek.waiting > 0)
1001
- bits.push(chip(`${peek.waiting} waiting on you`, "/", "badge-open"));
1002
- if (peek.running > 0)
1003
- bits.push(chip(`${peek.running} running`, "/runs", "badge-parked"));
1004
- if (peek.queued > 0)
1005
- bits.push(chip(`${peek.queued} queued`, "/board?view=order", "badge-queued"));
1006
- if (peek.doneRecently > 0)
1007
- bits.push(chip(`${peek.doneRecently} built today`, "/done", "badge-parked"));
1008
- return bits.length === 0 ? html `<span class="meta">quiet — nothing queued or waiting</span>` : joinHtml(bits, " ");
1009
- };
1010
- const projectCard = (one) => joinHtml([
1011
- html `<div class="card project-card">`,
1012
- html `<div class="row">${open !== null && open === one.path
1013
- ? html `<a class="project-name" href="${returnTo}"><strong>${one.name}</strong></a>`
1014
- : openForm(one.path, one.name, returnTo, "project-name")}`,
1015
- html `<span class="right">${open !== null && open === one.path ? html `<span class="badge badge-done">Open now</span>` : openForm(one.path, "Open", returnTo, "button-link")}</span></div>`,
1016
- html `<p class="meta mono" style="overflow-wrap:anywhere;margin:.2rem 0">${one.path}</p>`,
1017
- html `<p class="row" style="gap:.35rem;flex-wrap:wrap">${peekChips(one.path, one.peek)}</p>`,
1018
- html `<p class="meta row" style="justify-content:space-between;margin-bottom:0"><span>${one.note}</span><a href="/settings/knowledge?repo=${encodeURIComponent(one.path)}" style="display:inline-flex;align-items:center;min-height:44px">Knowledge</a></p>`,
1019
- html `</div>`,
1020
- ], "\n");
1021
- const cards = (items) => joinHtml(items.map(one => projectCard({ ...one, peek: peeks[one.path] ?? null })), "\n");
1022
- const recentItems = recent.map(one => ({ path: one.path, name: one.name, note: `last opened ${when(one.lastOpenedAt)}` }));
1023
- const candidateItems = candidates.map(path => ({ path, name: projectName(path), note: "seen in the queue" }));
1024
- // The rebuilt page's rows (shadcn/ui): the same projects, peeks and roads.
1025
- const home = homedir();
1026
- const rowOf = (path, name, openedAt) => {
1027
- const peek = peeks[path] ?? null;
1028
- return {
1029
- name, path, shortPath: path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path, open: open !== null && open === path, openedAt,
1030
- knowledgeHref: `/settings/knowledge?repo=${encodeURIComponent(path)}`,
1031
- ...(pullRequestsOn === undefined ? {} : { pullRequests: { href: `/settings/pull-requests?repo=${encodeURIComponent(path)}`, on: pullRequestsOn(path) } }),
1032
- ...(checkLevelOf === undefined ? {} : { checks: { href: `/settings/checks?repo=${encodeURIComponent(path)}`, level: CHECK_LEVEL_WORDS[checkLevelOf(path)] } }),
1033
- peek: peek === null ? null : [
1034
- ...(peek.waiting > 0 ? [{ label: `${peek.waiting} waiting on you`, href: "/", tone: "attention" }] : []),
1035
- ...(peek.running > 0 ? [{ label: `${peek.running} running`, href: "/runs", tone: "neutral" }] : []),
1036
- ...(peek.queued > 0 ? [{ label: `${peek.queued} queued`, href: "/board?view=order", tone: "neutral" }] : []),
1037
- ...(peek.doneRecently > 0 ? [{ label: `${peek.doneRecently} built today`, href: "/done", tone: "neutral" }] : []),
1038
- ],
1039
- };
1040
- };
1041
- // The two ways to ADD a project are the page's large, primary actions:
1042
- // browse this machine, or choose a GitHub repository. Manual path entry
1043
- // stays available as the clearly secondary expert road.
1044
- const addAction = (href, paths, title, detail) => html `<a class="project-add-action" href="${href}"><span class="project-add-icon">${strokeIcon(paths)}</span><span class="project-add-copy"><strong>${title}</strong><small>${detail}</small></span><span class="project-add-arrow" aria-hidden="true">→</span></a>`;
1045
- const addForms = joinHtml([
1046
- browsable || onboard !== null && !onboard.enabled && onboard.why.includes('--project-root')
1047
- ? null
1048
- : html `<p class="meta">Choose a projects folder once with <code>toolroll up --project-root &lt;dir&gt;</code>. Toolroll remembers it after that.</p>`,
1049
- onboardCard === null ? null : html `<div style="margin-top:.5rem">${onboardCard}</div>`,
1050
- html `<details class="project-add-more"><summary>Enter an exact path instead</summary>`,
1051
- html `${postForm("/projects/open", joinHtml([
1052
- html `<label>Path on this server<input type="text" name="path" placeholder="/Users/you/code/your-repo"></label>`,
1053
- html `<button type="submit">Open project</button>`,
1054
- ], "\n"), { attrs: { class: "card" }, returnTo })}</details>`,
1055
- ], "\n");
1056
- const addCard = joinHtml([
1057
- html `<div class="card project-add-card">`,
1058
- html `<h2 class="project-add-title">Add a project</h2>`,
1059
- html `<div class="project-add-actions">`,
1060
- browsable
1061
- ? addAction("/projects/browse", FOLDER_PATHS, "Choose a local folder", "Browse the project folders on this machine")
1062
- : null,
1063
- onboard === null
1064
- ? null
1065
- : addAction("/projects/github", html `<circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M6 9v6"/><path d="M18 9a9 9 0 0 1-9 9"/>`, "Add from GitHub", "Choose from repositories available to your GitHub login"),
1066
- html `</div>`,
1067
- addForms,
1068
- html `</div>`,
1069
- ], "\n");
1070
- // Arriving from New task with no project open: say what this step is for.
1071
- const choosing = returnTo.startsWith("/tasks/new");
1072
- return screen("projects", joinHtml([
1073
- choosing ? html `<h1>New task</h1><p class="meta">Choose the project it belongs to.</p>` : html `<h1>Projects</h1>`,
1074
- choosing ? null : html `<p class="meta">Add a folder or GitHub repository once. Its tasks and chat stay here.</p>`,
1075
- problem === null ? null : html `<div class="problem">${problem}</div>`,
1076
- recentItems.length === 0 && candidateItems.length === 0
1077
- ? html `<div class="card"><p><strong>Nothing to open yet.</strong></p><p class="meta">Add one below — opening it registers it here for next time.</p></div>`
1078
- : null,
1079
- activityHtml,
1080
- recentItems.length > 0 ? html `<h2>Recent</h2>${cards(recentItems)}` : null,
1081
- candidateItems.length > 0 ? html `<h2>Available</h2>${cards(candidateItems)}` : null,
1082
- addCard,
1083
- ], "\n"), { chrome, workspace: { view: {
1084
- kind: "projects", choosing, problem, returnTo, ...(activity === undefined ? {} : { activity }),
1085
- recent: recent.map(one => rowOf(one.path, one.name, one.lastOpenedAt)),
1086
- available: candidates.map(path => rowOf(path, projectName(path), null)),
1087
- add: { browse: browsable ? "/projects/browse" : null, github: onboard === null ? null : "/projects/github", html: htmlString(addForms) },
1088
- } } });
1089
- }
1090
792
  /** The no-script "move to the front" sentinel: the form cannot name the
1091
793
  * front of a partition, so the handler resolves it (slice 1b, fix 1). */
1092
794
  export const QUEUE_FRONT = "__TOP__";
@@ -1125,83 +827,6 @@ export function queueBody(tasks, workers, revision, queueRevision) {
1125
827
  : html `<p class="meta mono">${worker.building} building in this project · unattended capacity ${worker.capacity}</p>${postForm("/queue/note", html `<input type="text" name="note" value="${worker.note ?? ""}" data-initial="${worker.note ?? ""}" placeholder="what this worker is working through" aria-label="column note" maxlength="200"><button type="submit">Save</button>`, { attrs: { class: "row" }, projectRevision: revision, hidden: { runner: worker.name } })}<p class="meta">Takes from the shared queue when this column is empty</p>`;
1126
828
  return html `<div class="lanes" data-queue-revision="${queueRevision}">${column("shared queue", "anyone", html `<p class="meta">Workers take from here when their column is empty — top card first</p>`, shared, "nothing waiting — every task is reserved or running")}${joinHtml(workers.map(worker => column(worker.name + (worker.retired ? " (retired)" : ""), worker.name, noteForm(worker), columnOf(worker.name), "nothing queued — this worker will take from the shared queue")), "\n")}</div>`;
1127
829
  }
1128
- /**
1129
- * The fleet screen — one lane per runner, and the work in front of it.
1130
- * Building claims pin to the top of their worker's lane (live — never
1131
- * draggable); queued reservations sit below it (draggable to another
1132
- * worker, re-reserving them). Every card wears its project chip, which is
1133
- * the whole point: which agent is on which project is the page's answer.
1134
- * Form-free like the board, except the per-worker note, so the lane stack
1135
- * can re-render itself while somebody watches.
1136
- */
1137
- export function fleetBody(queued, building, runners, queueRevision, visibleRepo) {
1138
- const chip = (repo) => repo === null ? null : html ` <span class="badge">${projectName(repo)}</span>`;
1139
- const queuedCardOf = (one) => html `<div class="lane-card queue-card" data-task="${one.id}" data-taken="${one.taken ? "1" : "0"}"><p class="row">${one.taken ? null : GRIP_HANDLE}<a href="${taskHref(one.id)}">${one.title}</a></p><p class="row meta"><span class="mono">${one.id}</span>${chip(one.repo)}${one.approved ? null : html ` <span class="badge">Unapproved scope</span>`}${one.blockers > 0 ? html ` <span class="badge">Waits for ${one.blockers}</span>` : null}${one.taken ? html ` <span class="badge">Being taken</span>` : null}</p></div>`;
1140
- const lanes = runners.map(runner => {
1141
- const own = queued.filter(one => one.assignedRunner === runner.name && visibleRepo(one.repo));
1142
- const live = building.filter(one => one.runner === runner.name);
1143
- const retired = runner.retiredAt !== null;
1144
- const head = retired
1145
- ? html `<p class="meta">This worker is retired — drag these elsewhere, or register the name again</p>`
1146
- : html `${postForm("/queue/note", html `<input type="text" name="note" class="runner-note" value="${runner.queueNote ?? ""}" data-initial="${runner.queueNote ?? ""}" placeholder="what this worker is working through" aria-label="column note" maxlength="200">`, { attrs: { class: "row" }, hidden: { from: "fleet", runner: runner.name } })}<p class="meta">${runnerAlive(runner, new Date()) ? "alive" : "quiet"} · ${live.length}/${runner.capacity} building</p>`;
1147
- const buildingCards = joinHtml(live.map(claim => html `<div class="lane-card" data-taken="1"><p class="row"><span class="dot dot-ok pulse"></span> ${claim.taskId}</p><p class="row meta">building${chip(claim.repo ?? null)}${claim.model === null ? null : html ` · ${modelName(null, claim.model)}`} · ${Math.max(1, Math.round((Date.now() - new Date(claim.claimedAt).getTime()) / 60_000))}m</p></div>`), "\n");
1148
- const queuedCards = joinHtml(own.map(queuedCardOf), "\n");
1149
- const empty = live.length === 0 && own.length === 0
1150
- ? html `<p class="meta lane-empty">${retired ? "nothing left" : "idle — will take from the shared queue"}</p>`
1151
- : null;
1152
- return html `<section class="lane queue-column${live.length > 0 ? " lane-live" : ""}" data-column="${runner.name}"><h2>${runner.name}${retired ? " (retired)" : ""}</h2>${head}${buildingCards}${queuedCards}${empty}</section>`;
1153
- });
1154
- // The shared queue: anything reserved for nobody.
1155
- const shared = queued.filter(one => one.assignedRunner === null && visibleRepo(one.repo));
1156
- const sharedCards = joinHtml(shared.map(queuedCardOf), "\n");
1157
- const sharedLane = html `<section class="lane queue-column" data-column="anyone"><h2>Shared queue</h2><p class="meta">Any free worker takes from here, top first</p>${sharedCards}${shared.length === 0 ? html `<p class="meta lane-empty">nothing waiting — every task is reserved or running</p>` : null}</section>`;
1158
- return html `<div class="lanes" data-queue-revision="${queueRevision}">${sharedLane}${joinHtml(lanes, "\n")}</div>`;
1159
- }
1160
- /** Identical drag mechanics to the queue — the pointer events land on the worker's column. */
1161
- export function fleetScript() {
1162
- return (`(function(){var region=document.getElementById("fleet-region");if(!region)return;` +
1163
- `var stamp=document.getElementById("fleet-region-stamp");var dragging=null;` +
1164
- `function dirty(){if(region.contains(document.activeElement)&&document.activeElement!==document.body)return true;` +
1165
- `var inputs=region.querySelectorAll("input[type=text]");for(var i=0;i<inputs.length;i++){` +
1166
- `if(inputs[i].value!==(inputs[i].getAttribute("data-initial")||""))return true;}` +
1167
- `return false;}` +
1168
- `function paused(){return dragging!==null||dirty();}` +
1169
- `var wait=12000;var last=Date.now();var busy=false;` +
1170
- `function tell(){if(!stamp)return;if(paused()){stamp.textContent="paused while you edit";return;}` +
1171
- `stamp.textContent="updated "+Math.round((Date.now()-last)/1000)+"s ago";}setInterval(tell,1000);` +
1172
- `function cycle(){if(document.hidden||busy||paused()){setTimeout(cycle,wait);return;}busy=true;` +
1173
- `fetch("/fleet?fragment=1",{redirect:"manual",cache:"no-store"})` +
1174
- `.then(function(r){if(r.type==="opaqueredirect"||r.status===401||r.status===403){location.href="/login";return null;}` +
1175
- `return r.ok?r.text():null;})` +
1176
- `.then(function(t){if(t&&!paused()){region.innerHTML=t;last=Date.now();}})` +
1177
- `.catch(function(){})` +
1178
- `.then(function(){busy=false;tell();setTimeout(cycle,wait);});}setTimeout(cycle,wait);` +
1179
- `region.addEventListener("pointerdown",function(e){var handle=e.target.closest(".queue-handle");if(!handle)return;` +
1180
- `var card=handle.closest(".queue-card");if(!card||card.getAttribute("data-taken")==="1")return;` +
1181
- `e.preventDefault();dragging={task:card.getAttribute("data-task"),card:card};card.style.opacity="0.5";});` +
1182
- `region.addEventListener("pointermove",function(e){if(!dragging)return;e.preventDefault();` +
1183
- `var over=document.elementFromPoint(e.clientX,e.clientY);if(!over)return;` +
1184
- `var target=over.closest(".queue-card");var lane=over.closest(".queue-column");` +
1185
- `region.querySelectorAll(".queue-card,.queue-column").forEach(function(n){n.style.outline="";});` +
1186
- `if(target&&target!==dragging.card){target.style.outline="2px solid currentColor";}` +
1187
- `else if(lane){lane.style.outline="2px dashed currentColor";}});` +
1188
- `region.addEventListener("pointerup",function(e){if(!dragging)return;var drag=dragging;dragging=null;` +
1189
- `drag.card.style.opacity="";region.querySelectorAll(".queue-card,.queue-column").forEach(function(n){n.style.outline="";});` +
1190
- `var over=document.elementFromPoint(e.clientX,e.clientY);if(!over){tell();return;}` +
1191
- `var target=over.closest(".queue-card");var lane=over.closest(".queue-column");if(!lane){tell();return;}` +
1192
- `var column=lane.getAttribute("data-column");var before=target&&target!==drag.card?target.getAttribute("data-task"):"";` +
1193
- `if(target&&target.getAttribute("data-taken")==="1"){before="";}` +
1194
- `var wrap=region.querySelector("[data-queue-revision]");` +
1195
- `var fields={respond:"fragment",csrf:(region.querySelector("input[name=csrf]")||{value:""}).value,` +
1196
- `queueRevision:wrap?wrap.getAttribute("data-queue-revision"):"",task:drag.task,column:column,before:before};` +
1197
- `var post=new URLSearchParams();Object.keys(fields).forEach(function(k){post.append(k,fields[k]);});` +
1198
- `fetch("/queue/move",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:post.toString(),redirect:"manual"})` +
1199
- `.then(function(r){return r.ok?fetch("/fleet?fragment=1",{cache:"no-store"}).then(function(f){return f.ok?f.text():null;}):null;})` +
1200
- `.then(function(t){if(t&&!paused()){region.innerHTML=t;last=Date.now();}else if(t===null){location.href="/fleet";}})` +
1201
- `.catch(function(){})` +
1202
- `.then(function(){tell();});});` +
1203
- `})();`);
1204
- }
1205
830
  /** The /queue page's one nonce'd script: delegated pointer-event drag (it
1206
831
  * survives every fragment swap — finding 17) plus a poller that re-checks
1207
832
  * focus, dirty inputs, and an in-flight drag AT SWAP TIME, never only
@@ -1268,83 +893,6 @@ export function queueScript() {
1268
893
  `.then(function(){tell();});});` +
1269
894
  `})();`);
1270
895
  }
1271
- export function newTaskPage(chrome, project,
1272
- /** undefined: keep the revision the refused form posted (its draft), so a project switch since still refuses. */
1273
- projectRevision, problem, candidates = [], permissionDefault = "auto", qualityDefault = "default", projects = [],
1274
- /** A shipped template's fields, or a refused filing's own words, back in the form. */
1275
- prefill = null) {
1276
- return screen("New task", joinHtml([
1277
- html `<section class="task-intake">`,
1278
- html `<div class="task-intake-hero"><h1>What should get done?</h1></div>`,
1279
- html `<p class="meta">Work you do often? <a href="/recipes">Use a saved recipe</a> or <a href="/recipes/new">create one</a>.</p>`,
1280
- problem === null ? null : html `<div class="problem">${problem}</div>`,
1281
- taskComposerHtml({
1282
- project,
1283
- ...(projectRevision === undefined ? {} : { projectRevision }),
1284
- prefill,
1285
- candidates,
1286
- permissionDefault,
1287
- qualityDefault,
1288
- projects,
1289
- }),
1290
- html `<p class="meta task-agent-note">Nothing builds until you approve the plan. <a href="/chat">Or start in chat.</a></p>`,
1291
- html `</section>`,
1292
- ], "\n"), { chrome });
1293
- }
1294
- /**
1295
- * The triage flow: everything waiting on a person, one card at a time.
1296
- * Full context ON the card, the act inline, and every act lands back here
1297
- * — clearing the queue is taps, not navigation. Read state travels in the
1298
- * URL (the bounded skip cursor), never in the session: two tabs cannot
1299
- * fight, and a shared link shows the same queue.
1300
- */
1301
- export function nextPage(chrome, data) {
1302
- const { item } = data;
1303
- if (item === null) {
1304
- const held = data.skipped.length;
1305
- return screen("next", joinHtml([
1306
- html `<h1>All clear</h1>`,
1307
- held > 0
1308
- ? html `<p>Nothing left except the ${held} you set aside. <a href="/next">Look at those again</a>, or come back later.</p>`
1309
- : html `<p>Nothing needs you. The machine is either working or waiting on its own clocks.</p>`,
1310
- html `<p class="meta"><a href="/board">the board</a> shows what is moving · <a href="/inbox">the inbox</a> lists everything at once</p>`,
1311
- ], "\n"), { chrome });
1312
- }
1313
- const skipHref = `/next?skip=${encodeURIComponent([...data.skipped, item.key].join(","))}`;
1314
- const header = html `<p class="meta next-pager"><span>${data.remaining === 1 ? "the last thing waiting on you" : `1 of ${data.remaining} waiting on you`}</span><a class="skip" href="${skipHref}">not now — next →</a></p>`;
1315
- let card;
1316
- if (item.kind === "decision") {
1317
- const { decision } = item;
1318
- card =
1319
- html `<h1>${decision.taskId} <span class="meta">asked ${whenTime(decision.createdAt)}</span></h1><div class="recap">${decision.recap}</div><div class="question">${decision.question}</div>${decisionOptionForms(decision, "next")}`;
1320
- }
1321
- else if (item.kind === "approval" && !consentDoorOf(data.scope, data.route).open) {
1322
- const door = consentDoorOf(data.scope, data.route);
1323
- card =
1324
- html `<h1>${item.approval.taskId}</h1><p>${item.approval.title}</p>${consentClosedHtml(item.approval.taskId, door, "next")}<p class="meta"><a href="${taskHref(item.approval.taskId)}">open the full task</a></p>`;
1325
- }
1326
- else if (item.kind === "approval") {
1327
- const scope = data.scope;
1328
- card =
1329
- html `<h1>${item.approval.taskId}</h1><p>${item.approval.title}</p>${data.planDocument === null
1330
- ? null
1331
- : html `<div class="card"><p><strong>The plan</strong> <span class="meta">drafted by a planning session</span></p>${executionPlanHtml(data.planDocument, true)}${planContractHtml(data.planContract ?? null, "ceremony")}</div>`}${postForm(`${taskHref(item.approval.taskId)}/approve`, html `<p><strong>Approve exactly this:</strong></p>${data.deliverable === "report" ? html `<p class="meta"><span class="badge">Scout</span> a read-only session investigates this goal and delivers a report — no branch, nothing changes in the repository</p>` : null}${scope === null || scope.profileState !== "unresolved" ? null : profileWords(scope)}${originNote(data.origin) === null ? null : html `<p class="meta" data-approval-origin>${originNote(data.origin)}</p>`}<p class="meta">Goal</p><p class="recap" style="margin-top:0">${scope?.goal ?? item.approval.goal}</p><p class="meta">Not this</p><p class="recap" style="margin-top:0">${scope?.outOfScope == null ? html `<em>no exclusions</em>` : scope.outOfScope}</p><p class="meta">Touches · ${scope === null || scope.touches.length === 0 ? "anything" : joinHtml(scope.touches, ", ")}</p>${scope === null ? null : acceptanceCeremonyHtml(scope.acceptance)}${
1332
- // The AGENTS the yes freezes (v48): the same concise block the task
1333
- // page and chat sign under, before the password — runtime limits one
1334
- // tap away, never in the way.
1335
- agentsCeremonyHtml(data.route)}${scope === null ? null : runtimeDetailsHtml(scope)}${reauth(html `<label>Your password, typed again — a signed-in session alone cannot agree to work<input type="password" name="token" autocomplete="current-password"></label>`)}<div class="sticky-actions"><button type="submit">Approve this scope</button></div>`, { attrs: { class: "card approve-form" }, returnTo: "next", hidden: { nonce: data.nonce, digest: data.approvalDigest ?? item.approval.digest } })}<p class="meta"><a href="${taskHref(item.approval.taskId)}">open the full task</a> to edit the scope first</p>`;
1336
- }
1337
- else if (item.kind === "requeue") {
1338
- card =
1339
- html `<h1>${item.stalled.taskId}</h1><p>${item.stalled.title}</p><p class="meta">Stopped — ${item.stalled.incidentCount} incident(s)${item.stalled.strikes > 0 ? html ` after ${item.stalled.strikes} attempt(s)` : null}</p>${postForm(`${taskHref(item.stalled.taskId)}/requeue`, html `<p class="meta">Requeue resolves the incidents, clears the failed attempts, and puts it back in line</p><button type="submit">Retry this work</button>`, { attrs: { class: "card" }, returnTo: "next" })}<p class="meta"><a href="${taskHref(item.stalled.taskId)}">open the full task</a> to read the runs first</p>`;
1340
- }
1341
- else {
1342
- const { gap } = item;
1343
- card =
1344
- html `<h1>Supply ${gap.key}</h1><p class="meta">${gap.state}</p><p>Filling this starts ${gap.unblocks.length} task(s): ${joinHtml(gap.unblocks.map(one => html `<span class="mono">${one}</span>`), ", ")}</p><div class="card"><p class="meta">Prove it filled from the terminal:</p><pre class="recap">${gap.verify}</pre></div>`;
1345
- }
1346
- return screen("next", joinHtml([header, card], "\n"), { chrome });
1347
- }
1348
896
  /** Put away old tasks (docs/plans/task-tidy.md): exactly what would be put away, grouped, each with why, every one
1349
897
  * ticked; one step-up puts away the ticked ones, bound to this preview's digest. The CLI twin is named beside it. */
1350
898
  export function putAwayPage(chrome, data) {