toolroll 0.9.86 → 0.9.88

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 (173) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/dist/acp-check.d.ts +9 -0
  3. package/dist/acp-check.js +40 -28
  4. package/dist/agents-board.d.ts +12 -0
  5. package/dist/agents-board.js +26 -0
  6. package/dist/approval-rules-ui.d.ts +4 -1
  7. package/dist/approval-rules-ui.js +31 -15
  8. package/dist/assignment-ui.js +1 -1
  9. package/dist/backup-ui.d.ts +5 -4
  10. package/dist/backup-ui.js +65 -32
  11. package/dist/browser/terminal.css +1 -1
  12. package/dist/browser/terminal.js +57 -54
  13. package/dist/browser/workspace.css +2 -2
  14. package/dist/browser/workspace.js +35 -70
  15. package/dist/browser-crew.js +1 -1
  16. package/dist/browser-workspace.d.ts +329 -183
  17. package/dist/browser-workspace.js +10 -9
  18. package/dist/chat-controls.d.ts +1 -1
  19. package/dist/chat-controls.js +1 -1
  20. package/dist/chat-settings-page.d.ts +29 -9
  21. package/dist/chat-settings-page.js +59 -28
  22. package/dist/check-levels-ui.d.ts +2 -1
  23. package/dist/check-levels-ui.js +67 -58
  24. package/dist/contracts/cli.d.ts +43 -0
  25. package/dist/contracts/cli.js +4 -0
  26. package/dist/contracts/console-api.d.ts +1 -1
  27. package/dist/contracts/console-api.js +1 -1
  28. package/dist/credentials-ui.d.ts +5 -3
  29. package/dist/credentials-ui.js +58 -31
  30. package/dist/design/kit.css +722 -0
  31. package/dist/design/kit.d.ts +208 -0
  32. package/dist/design/kit.js +177 -0
  33. package/dist/design/tokens.css +31 -1
  34. package/dist/discord-settings.js +8 -4
  35. package/dist/export-ui.d.ts +6 -4
  36. package/dist/export-ui.js +18 -8
  37. package/dist/flow-changed.d.ts +14 -0
  38. package/dist/flow-changed.js +53 -0
  39. package/dist/flow-gallery-ui.d.ts +41 -4
  40. package/dist/flow-gallery-ui.js +95 -63
  41. package/dist/flow-starters-ui.d.ts +3 -9
  42. package/dist/flow-starters-ui.js +12 -23
  43. package/dist/flow-unattended-words.d.ts +20 -0
  44. package/dist/flow-unattended-words.js +24 -0
  45. package/dist/flow-unattended.d.ts +9 -5
  46. package/dist/flow-unattended.js +45 -23
  47. package/dist/flows-cli.js +7 -3
  48. package/dist/flows-ui.d.ts +12 -6
  49. package/dist/flows-ui.js +141 -30
  50. package/dist/integrations-ui.d.ts +6 -4
  51. package/dist/integrations-ui.js +34 -40
  52. package/dist/integrations.d.ts +2 -2
  53. package/dist/integrations.js +4 -4
  54. package/dist/kits-ui.d.ts +8 -10
  55. package/dist/kits-ui.js +29 -33
  56. package/dist/kits.js +1 -1
  57. package/dist/knowledge-ui.js +76 -19
  58. package/dist/ledger-view.d.ts +2 -0
  59. package/dist/ledger-view.js +41 -38
  60. package/dist/models-ui.d.ts +8 -3
  61. package/dist/models-ui.js +82 -59
  62. package/dist/monitoring-ui.d.ts +4 -2
  63. package/dist/monitoring-ui.js +44 -13
  64. package/dist/needs-you.d.ts +1 -1
  65. package/dist/needs-you.js +8 -0
  66. package/dist/operate-remote-arguments.js +1 -0
  67. package/dist/operate-remote.js +1 -1
  68. package/dist/operate.d.ts +1 -1
  69. package/dist/operate.js +38 -4
  70. package/dist/passwordless.d.ts +1 -1
  71. package/dist/passwordless.js +1 -1
  72. package/dist/people-audit-ui.d.ts +2 -1
  73. package/dist/people-audit-ui.js +25 -23
  74. package/dist/policy-ui.d.ts +3 -2
  75. package/dist/policy-ui.js +27 -21
  76. package/dist/policy.d.ts +3 -0
  77. package/dist/policy.js +14 -1
  78. package/dist/project-delete-ui.d.ts +4 -2
  79. package/dist/project-delete-ui.js +135 -126
  80. package/dist/project-knowledge.d.ts +10 -10
  81. package/dist/publish.d.ts +11 -0
  82. package/dist/publish.js +16 -3
  83. package/dist/pull-request-ui.d.ts +2 -1
  84. package/dist/pull-request-ui.js +39 -28
  85. package/dist/recipe-ui.d.ts +4 -2
  86. package/dist/recipe-ui.js +14 -4
  87. package/dist/release-adapters.d.ts +3 -0
  88. package/dist/release-adapters.js +22 -2
  89. package/dist/release-cli.js +81 -9
  90. package/dist/release.d.ts +23 -0
  91. package/dist/release.js +46 -4
  92. package/dist/releases.js +1 -1
  93. package/dist/request-budget-ui.d.ts +4 -2
  94. package/dist/request-budget-ui.js +30 -19
  95. package/dist/retention-ui.d.ts +4 -3
  96. package/dist/retention-ui.js +15 -12
  97. package/dist/scope.d.ts +3 -0
  98. package/dist/scope.js +6 -0
  99. package/dist/serve.d.ts +0 -1
  100. package/dist/serve.js +51 -7
  101. package/dist/server/agents-computers.d.ts +9 -0
  102. package/dist/server/agents-computers.js +67 -0
  103. package/dist/server/chat.js +16 -16
  104. package/dist/server/chrome.d.ts +1 -1
  105. package/dist/server/chrome.js +19 -9
  106. package/dist/server/console.css +24 -12
  107. package/dist/server/flows.js +64 -36
  108. package/dist/server/home.d.ts +35 -0
  109. package/dist/server/home.js +104 -0
  110. package/dist/server/http.d.ts +3 -0
  111. package/dist/server/http.js +5 -0
  112. package/dist/server/kit-standalone.d.ts +22 -0
  113. package/dist/server/kit-standalone.js +70 -0
  114. package/dist/server/kit.d.ts +419 -0
  115. package/dist/server/kit.js +369 -0
  116. package/dist/server/pages.d.ts +2 -1
  117. package/dist/server/pages.js +213 -174
  118. package/dist/server/people-tokens.js +26 -85
  119. package/dist/server/render-history.d.ts +51 -0
  120. package/dist/server/render-history.js +228 -0
  121. package/dist/server/render-new-task.d.ts +29 -0
  122. package/dist/server/render-new-task.js +96 -0
  123. package/dist/server/render-next.d.ts +63 -0
  124. package/dist/server/render-next.js +109 -0
  125. package/dist/server/render-pages.d.ts +22 -190
  126. package/dist/server/render-pages.js +123 -575
  127. package/dist/server/render-people.d.ts +24 -3
  128. package/dist/server/render-people.js +93 -72
  129. package/dist/server/render-projects.d.ts +103 -0
  130. package/dist/server/render-projects.js +247 -0
  131. package/dist/server/render-results.d.ts +5 -24
  132. package/dist/server/render-results.js +48 -185
  133. package/dist/server/render-settings.d.ts +5 -74
  134. package/dist/server/render-settings.js +102 -493
  135. package/dist/server/render-tasks.d.ts +35 -5
  136. package/dist/server/render-tasks.js +114 -85
  137. package/dist/server/route-table.js +20 -5
  138. package/dist/server/settings.js +276 -255
  139. package/dist/server/task-views.d.ts +8 -0
  140. package/dist/server/task-views.js +85 -2
  141. package/dist/server/tasks.d.ts +8 -0
  142. package/dist/server/tasks.js +95 -40
  143. package/dist/server/terminals.js +1 -1
  144. package/dist/server/welcome.d.ts +22 -0
  145. package/dist/server/welcome.js +69 -0
  146. package/dist/settings-groups-ui.d.ts +279 -0
  147. package/dist/settings-groups-ui.js +615 -0
  148. package/dist/settings-kit.d.ts +42 -6
  149. package/dist/settings-kit.js +69 -7
  150. package/dist/setup-guide.d.ts +2 -2
  151. package/dist/skills-ui.js +59 -7
  152. package/dist/slack-settings.js +12 -5
  153. package/dist/spend-ui.d.ts +45 -26
  154. package/dist/spend-ui.js +124 -56
  155. package/dist/sso-ui.d.ts +5 -2
  156. package/dist/sso-ui.js +63 -31
  157. package/dist/storage-ui.d.ts +3 -2
  158. package/dist/storage-ui.js +56 -50
  159. package/dist/store.d.ts +112 -3
  160. package/dist/store.js +292 -20
  161. package/dist/surface.js +5 -1
  162. package/dist/teams-settings.d.ts +2 -1
  163. package/dist/teams-settings.js +24 -41
  164. package/dist/telegram-settings.d.ts +0 -4
  165. package/dist/telegram-settings.js +52 -25
  166. package/dist/toolroll-update-ui.d.ts +8 -4
  167. package/dist/toolroll-update-ui.js +55 -45
  168. package/dist/tools-ui.d.ts +5 -3
  169. package/dist/tools-ui.js +107 -42
  170. package/dist/work-index.js +11 -7
  171. package/dist/workspace-ui.d.ts +1 -1
  172. package/dist/workspace-ui.js +7 -4
  173. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { releaseProgressFor, type ShipChecks } from "../store.js";
1
2
  import { type Html } from "../html.js";
2
3
  import { type AssignmentSnapshot } from '../assignment.js';
3
4
  import { type FailureExplanation } from '../needs-you.js';
@@ -12,6 +13,7 @@ import { type CompletionReceiptView, type ResultDetail, type TaskPullRequest } f
12
13
  import { taskBody, type MilestoneProgressView, type PlanContractView, type PlanRevisionLedgerView, type RevisionDocView, type RevisionView, type RouteView } from "./render-tasks.js";
13
14
  import { type Who } from "./session.js";
14
15
  import type { TaskPageExtras } from "./runtime.js";
16
+ import type { BrowserTaskView } from "../browser-workspace.js";
15
17
  import type { PlanRevision, TaskRef } from "../store.js";
16
18
  import { type Run, type Store, type Task, type TaskState } from "../store.js";
17
19
  import { type ReviewFacts } from "../workspace-ui.js";
@@ -88,3 +90,9 @@ export declare function createTaskViews(runtime: TaskViewsRuntime): {
88
90
  progressOf: (taskRef: number, currentDocument: string | null) => MilestoneProgressView[] | null;
89
91
  resultDetailOf: (run: Run, who: Who, now: Date) => ResultDetail;
90
92
  };
93
+ /** A pull request's checks as Ship shows them (src/design/KIT.md timing rows): each check on one elapsed-time axis
94
+ * from the first one's start, its state in the five words' tones, and one line for the whole. Null: no checks read. */
95
+ export declare function shipViewOf(ship: ShipChecks | null, now: Date): NonNullable<BrowserTaskView["ship"]> | null;
96
+ /** A release's steps as its progress view shows them: every step in order on one time axis, the ones not reached
97
+ * queued, and where it stands in one line. Null: this task is no release. */
98
+ export declare function releaseStepsViewOf(progress: ReturnType<typeof releaseProgressFor>, now: Date): NonNullable<BrowserTaskView["release"]> | null;
@@ -1,6 +1,8 @@
1
1
  import { feedbackImageProblem } from "../result-actions.js";
2
2
  import { parseFeedbackImage } from "../result-review.js";
3
3
  import { releaseViewOf } from "../release-view.js";
4
+ import { RELEASE_STEPS } from "../release.js";
5
+ import { releaseProgressFor, shipChecksForTask } from "../store.js";
4
6
  import { releaseCiRunningOf } from "../release-ci-hold.js";
5
7
  import { releaseObligationsOf } from "../release-recheck.js";
6
8
  import { approvalProofOf, freshlyApprovable, freshnessFor, withFreshness } from "../scope.js";
@@ -836,8 +838,12 @@ export function createTaskViews(runtime) {
836
838
  } })() } } : {}) } : page.workspace?.view;
837
839
  // A refusal that keeps a draft (a scope edit, an armed cancel) or asks for the plan editor reopens on Plan, where the
838
840
  // draft and its error are; anything else on Activity unless the address named a tab.
839
- const tab = extras.tab ?? (scopeDraft !== undefined || cancelDraft !== undefined || editPlan ? "plan" : "activity");
840
- const oneTaskView = taskView?.kind === "task" ? onePageView(taskView, family, presentedData.family?.runs ?? data.runs.map(run => ({ ...run, taskId: data.task.id })), { ...extras, tab }) : taskView;
841
+ const tab = extras.tab ?? "plan";
842
+ const onePage = taskView?.kind === "task" ? onePageView(taskView, family, presentedData.family?.runs ?? data.runs.map(run => ({ ...run, taskId: data.task.id })), { ...extras, tab }) : taskView;
843
+ // Ship (its pull request's checks, each on one time axis) and a release's own steps, read from the store now.
844
+ const rootId = family?.root.id ?? taskId;
845
+ const oneTaskView = onePage?.kind === "task" ? { ...onePage, ship: shipViewOf(shipChecksForTask(runtime.store, rootId), runtime.clock()),
846
+ release: releaseStepsViewOf(releaseProgressFor(runtime.store, rootId), runtime.clock()) } : onePage;
841
847
  // The Result tab's no-script page, under the task's own (the same HTML its result page serves), and its script.
842
848
  if (extras.result != null) {
843
849
  page.body = html `${page.body}<section class="task-result-tab" id="task-result" aria-label="Result">${extras.result.html}</section>`;
@@ -1163,3 +1169,80 @@ function autoReleaseOf(scope) {
1163
1169
  return scope.approvalBasis === "auto-green" ? { kind: "approved", candidate: scope.candidate }
1164
1170
  : scope.approvalBasis === "auto-check" ? { kind: "checking", candidate: scope.candidate } : null;
1165
1171
  }
1172
+ /** A pull request's checks as Ship shows them (src/design/KIT.md timing rows): each check on one elapsed-time axis
1173
+ * from the first one's start, its state in the five words' tones, and one line for the whole. Null: no checks read. */
1174
+ export function shipViewOf(ship, now) {
1175
+ if (ship === null || ship.checks.length === 0)
1176
+ return null;
1177
+ const at = (iso) => iso === null ? null : Date.parse(iso);
1178
+ const observed = Date.parse(ship.observedAt);
1179
+ const end = Number.isFinite(observed) ? observed : now.getTime();
1180
+ const starts = ship.checks.map(one => at(one.startedAt)).filter((one) => one !== null && Number.isFinite(one));
1181
+ const origin = starts.length === 0 ? end : Math.min(...starts);
1182
+ const st = (state) => state === "passed" ? "ok" : state === "failed" || state === "cancelled" ? "bad" : state === "running" ? "run" : state === "queued" ? "q" : "ok";
1183
+ const rows = ship.checks.map(one => {
1184
+ const start = at(one.startedAt), done = at(one.completedAt);
1185
+ const from = start ?? origin;
1186
+ return { name: one.name, ...(one.workflow === null || one.workflow === one.name ? {} : { sub: one.workflow }),
1187
+ ...(one.state === "skipped" ? { sub: "skipped" } : one.state === "neutral" ? { sub: "neutral" } : {}),
1188
+ st: st(one.state), start: Math.max(0, (from - origin) / 1000), dur: start === null ? 0 : Math.max(0, ((done ?? end) - start) / 1000) };
1189
+ });
1190
+ const passed = ship.checks.filter(one => one.state === "passed" || one.state === "skipped" || one.state === "neutral").length;
1191
+ const failed = ship.checks.filter(one => one.state === "failed" || one.state === "cancelled").length;
1192
+ const running = ship.checks.length - passed - failed;
1193
+ // The overall status is the observation's own (the one merges read); the rows only time each check.
1194
+ // `none` (the pull request closed or merged, or GitHub reported no checks) is never painted as passed.
1195
+ const state = ship.observed === "failing" ? "failed" : ship.observed === "passing" ? "passed" : ship.observed === "running" ? "running" : "none";
1196
+ return {
1197
+ pr: ship.prNumber, head: ship.headSha.slice(0, 7), state, rows,
1198
+ summary: state === "failed" ? (failed > 0 ? `${failed} of ${ship.checks.length} ${ship.checks.length === 1 ? "check" : "checks"} failed` : "Checks are failing on GitHub")
1199
+ : state === "running" ? `${passed} of ${ship.checks.length} checks passed` : state === "passed" ? `All ${ship.checks.length} ${ship.checks.length === 1 ? "check" : "checks"} passed`
1200
+ : "No current checks: the pull request is closed or merged, or GitHub reported none",
1201
+ observedAt: ship.observedAt,
1202
+ };
1203
+ }
1204
+ /** A release step's name in words (release.ts RELEASE_STEPS). */
1205
+ const RELEASE_STEP_WORDS = {
1206
+ gate: "Release gate", approval: "Approval", check: "Release check", complete: "Complete the task", quiet: "Wait for quiet", deploy: "Deploy to this Mac",
1207
+ link: "Point the CLI at it", ci: "Pull request checks", merge: "Merge into main", tree: "Verify the merged tree", tag: "Tag", publish: "Publish to npm", homebrew: "Homebrew",
1208
+ };
1209
+ /** A release's steps as its progress view shows them: every step in order on one time axis, the ones not reached
1210
+ * queued, and where it stands in one line. Null: this task is no release. */
1211
+ export function releaseStepsViewOf(progress, now) {
1212
+ if (progress === null)
1213
+ return null;
1214
+ const origin = Date.parse(progress.startedAt);
1215
+ const recorded = new Map(progress.steps.map(one => [one.step, one]));
1216
+ const all = RELEASE_STEPS.filter(step => step !== "done");
1217
+ // A release that started part way (one carried under an earlier approval, or resumed from an older journal) shows
1218
+ // the steps from where its own record starts; a step missing between two recorded ones is a write that was lost.
1219
+ const firstAt = all.findIndex(step => recorded.has(step));
1220
+ const lastAt = all.reduce((at, step, index) => recorded.has(step) ? index : at, -1);
1221
+ const steps = firstAt <= 0 ? all : all.slice(firstAt);
1222
+ const rows = steps.map(step => {
1223
+ const one = recorded.get(step);
1224
+ // A step with no record is never painted as done: between recorded ones it says so, after them it is queued.
1225
+ if (one === undefined)
1226
+ return all.indexOf(step) < lastAt ? { name: RELEASE_STEP_WORDS[step] ?? step, sub: "not recorded", st: "q", start: 0, dur: 0 }
1227
+ : { name: RELEASE_STEP_WORDS[step] ?? step, st: "q", start: 0, dur: 0 };
1228
+ const start = Date.parse(one.startedAt), end = one.endedAt === null ? now.getTime() : Date.parse(one.endedAt);
1229
+ const st = one.state === "done" ? "ok" : one.state === "failed" ? "bad" : one.state === "waiting" ? "wait" : "run";
1230
+ return { name: RELEASE_STEP_WORDS[step] ?? step, ...(one.detail === null || one.state === "done" ? {} : { sub: one.detail.slice(0, 160) }), st,
1231
+ start: Number.isFinite(start) && Number.isFinite(origin) ? Math.max(0, (start - origin) / 1000) : 0, dur: Number.isFinite(start) && Number.isFinite(end) ? Math.max(0, (end - start) / 1000) : 0 };
1232
+ });
1233
+ const current = rows.findIndex(row => row.st === "run" || row.st === "wait" || row.st === "bad");
1234
+ // Released only on its own evidence: the last step (Homebrew, after publication) recorded done. A merge (authority
1235
+ // `merged`) is not a release: verifying the tree, the tag and publication still follow it.
1236
+ const done = progress.stopped === null && recorded.get("homebrew")?.state === "done";
1237
+ const at = current === -1 ? null : rows[current];
1238
+ const last = lastAt === -1 ? null : RELEASE_STEP_WORDS[all[lastAt]] ?? all[lastAt];
1239
+ return {
1240
+ tag: progress.tag, version: progress.version, pr: progress.pr,
1241
+ rows,
1242
+ state: progress.stopped !== null ? "failed" : done ? "done" : at?.st === "wait" ? "waiting" : "running",
1243
+ headline: done ? `Released ${progress.version}` : progress.stopped !== null ? `Stopped at ${RELEASE_STEP_WORDS[progress.stopped.step] ?? progress.stopped.step}`
1244
+ : at !== null ? `${at.name} · Step ${current + 1} of ${rows.length}` : last === null ? "Starting" : `${last} done · the next step starts on its own`,
1245
+ stopped: progress.stopped === null ? null : { step: RELEASE_STEP_WORDS[progress.stopped.step] ?? progress.stopped.step, message: progress.stopped.message, at: progress.stopped.at },
1246
+ startedAt: progress.startedAt,
1247
+ };
1248
+ }
@@ -46,3 +46,11 @@ export declare function createTidyHandlers(runtime: ServerRuntime): {
46
46
  "task.act.archive": (ctx: HandlerContext) => Promise<void>;
47
47
  "task.act.restore": (ctx: HandlerContext) => Promise<void>;
48
48
  };
49
+ /**
50
+ * Where a retired Tasks address goes now (wave A, src/design/KIT.md): Done is Finished. `board` names where the old
51
+ * Board's state view goes once it is retired (Tasks' Board view; its Order view and its live fragment stay). Null: this
52
+ * address still answers itself.
53
+ */
54
+ export declare function retiredTasksAddress(route: "board" | "done", url: URL): string | null;
55
+ /** How many cards a Board column shows; the column's count covers them all and its "View all" opens the list. */
56
+ export declare const BOARD_COLUMN_LIMIT = 40;
@@ -11,7 +11,7 @@ function returnedFlow(value) {
11
11
  import { originOf } from "../task-source.js";
12
12
  import { ACP_AGENTS, isAcpProvider } from "../acp-agents.js";
13
13
  import { handlersOf } from './handler-registry.js';
14
- import { closedQuestionWords } from "../decision.js";
14
+ import { closedQuestionWords, LIMITS } from "../decision.js";
15
15
  import { DEPRECATED_PAGE } from "../deprecations.js";
16
16
  /** tasks handlers, moved without changing their route bodies. */
17
17
  import { html, htmlString, joinHtml, postForm, textHtml } from "../html.js";
@@ -30,8 +30,11 @@ import { CodingActionError } from '../coding-workspace.js';
30
30
  import { checkResponse, CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
31
31
  import { hasForbiddenControls, validateNote } from "../decision.js";
32
32
  import { builderReadyFor, diagnoseTaskDispatch, firstRunBuilderRepo } from "../dispatch.js";
33
- import { withBuilderStep } from "../first-run.js";
34
- import { evidencePack, evidencePackHtml } from "../evidence-pack.js";
33
+ import { firstTaskSuggestions, withBuilderStep } from "../first-run.js";
34
+ import { newTaskPage } from "./render-new-task.js";
35
+ import { nextPage } from "./render-next.js";
36
+ import { evidencePack } from "../evidence-pack.js";
37
+ import { decisionPage, evidenceRecordPage, runPage } from "./render-history.js";
35
38
  import { readVerifiedArtifact, readVerifiedReport, writeEvidenceFile } from "../evidence.js";
36
39
  import { computeGaps } from "../gaps.js";
37
40
  import { limitsView } from "../limits-ui.js";
@@ -68,11 +71,11 @@ import { buildProgressOf, parseWorkView } from "../workspace-ui.js";
68
71
  import { requestContext } from "./request-context.js";
69
72
  import { boardTickScript, oneLineOf, QUEUE_VIEW, refuse, regionScript, screen, transcriptScript, whenTime } from "./chrome.js";
70
73
  import { chatReturnWithSaid, matchTaskPath, redirect, respond, reviewHref, safeChatReturn, safeReturn, SAFETY, taskChatHref, taskHref } from "./http.js";
71
- import { putAwayPage, boardBody, boardWorkers, donePage, inboxFingerprints, inboxPage, newTaskPage, nextPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, workPage } from "./render-pages.js";
74
+ import { putAwayPage, boardBody, boardWorkers, inboxFingerprints, inboxPage, parseInboxTab, QUEUE_FRONT, queueBody, queueScript, TASK_STATES, workPage } from "./render-pages.js";
72
75
  import { runEfficiencyOf } from "../run-efficiency.js";
73
76
  import { timingFor } from "../latency-timeline.js";
74
- import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitDetailParts, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge, runPage, RUNS_PAGE, runsPage, withRefusal } from "./render-results.js";
75
- import { approvalFormDigest, approveRefusalWords, checkProgressHtml, consentDoorOf, decisionPage, nonceHashOf, PermissionsWouldChange, requirementsFromEditor, resumeDigestOf, taskOf } from "./render-tasks.js";
77
+ import { ACCEPT_ANYWAY_NEEDS_REASON, editorFileHref, proofBundleView, rankReviewQueue, RESULT_REFUSALS, REVIEW_QUEUE_CAP, reviewCockpitDetailParts, reviewCockpitPage, reviewPriorityOf, runFactsFragment, runOutcomeBadge, withRefusal } from "./render-results.js";
78
+ import { approvalFormDigest, approveRefusalWords, checkProgressHtml, consentDoorOf, nonceHashOf, PermissionsWouldChange, requirementsFromEditor, resumeDigestOf, taskOf } from "./render-tasks.js";
76
79
  import {} from "./session.js";
77
80
  export function createTasksHandlers(runtime) {
78
81
  const { identities, store, peekSay, peekCache, PEEK_CACHE_TTL_MS, peekInFlight, PEEK_GLOBAL_INFLIGHT, peekBySession, PEEK_SESSION_INFLIGHT, clock, peekName, PEEK_FRAGMENT_BYTES, peekEvict, evidenceRoot, sendScreen, chromeFor, visible, consumeApprovalNonce, authenticateApprover, options, mintApprovalNonce, taskScreen, unscopedMode, admissionList, planViewOf, revisionViewOf, runIsTaskResult, leadPrincipal, familyOf, armTaskResume, runIsLive, revisionLedgerOf, resultDetailOf, reviewFactsFor, taskRepoOf, runVisible, restricted, codingActorAllowed, codingProjectAllowed, managedRepos, routeViewOf, workAccess, firstRunStepsNow, revisionDocOf, failureOf, dockedConversation, planContractViewOf, familiesInView, explainAttempt, taskChatFocus, familyTasksInView, liveCeiling, projectOf, ceiling, bustBadge, revisionDestination } = runtime;
@@ -251,7 +254,7 @@ export function createTasksHandlers(runtime) {
251
254
  // the database alone: a row reads no check log. A live build's row says which step it is on, or the step it is
252
255
  // stuck on, from that live attempt's own progress only, never a stopped attempt's; the step's words come from its
253
256
  // current plan (one saved file, for live rows only), and without it the row names the step by number.
254
- work = { ...work, items: work.items.map((item) => {
257
+ const enrich = (page) => ({ ...page, items: page.items.map((item) => {
255
258
  if (item.liveRunId !== null) {
256
259
  const ref = store.lookupRef(item.activeTaskId);
257
260
  const recorded = ref === null ? null : store.latestCheckpointForRun(item.liveRunId);
@@ -270,9 +273,18 @@ export function createTasksHandlers(runtime) {
270
273
  return item;
271
274
  const failure = failureOf(family.versions.flatMap(version => store.runsFor(version.refId)), null, false);
272
275
  return failure.kind === "reason" ? item : { ...item, status: { ...item.status, detail: failure.line } };
273
- }) };
276
+ }) });
277
+ work = enrich(work);
278
+ // The Board view: the same index as four columns (needs you, queued, running, finished), never a second classifier.
279
+ const board = url.searchParams.get("layout") !== "board" ? null : (() => {
280
+ const column = (view) => enrich(workIndexPage(store, now, workAccess(), { view, limit: BOARD_COLUMN_LIMIT, project, root: evidenceRoot, ...(state === null ? {} : { state }) }));
281
+ return { needs: column("needs-you"), queued: column("queued"), running: column("running"), finished: column("completed") };
282
+ })();
274
283
  const page = workPage(chromeFor(project, "work", undefined, rollup ? "all" : undefined), {
275
284
  view,
285
+ lens: project,
286
+ all: url.searchParams.get("all") === "1",
287
+ board,
276
288
  ...(url.searchParams.has('project') && project !== null ? { projectFilter: project } : {}),
277
289
  work,
278
290
  previous: url.searchParams.has('cursor'),
@@ -334,6 +346,9 @@ export function createTasksHandlers(runtime) {
334
346
  deliverable: ref?.deliverable ?? "branch",
335
347
  route,
336
348
  origin: originOf(store, item.approval.taskId, repo => store.accountCanAccess(who.name, repo)),
349
+ // Whether a remembered password may approve these exact terms: the task page's own preview (task-views.ts
350
+ // approvalFresh), read without minting a second nonce. Display only; the approve POST proves it again.
351
+ remembered: nonce !== "" && runtime.taskViewData(item.approval.taskId, who, null, { mintNonce: false })?.approvalFresh != null,
337
352
  csrf, nonce, remaining: remaining.length, skipped: [...skipped], now,
338
353
  }));
339
354
  }
@@ -588,9 +603,12 @@ export function createTasksHandlers(runtime) {
588
603
  reviewPage.workspace = { ...reviewPage.workspace, conversation: reviewDocked, pageHtml: reviewPage.body };
589
604
  return sendScreen(response, 200, reviewPage);
590
605
  }
606
+ /** Finished tasks live in the Tasks list's Finished view now (UI revamp wave B): the old address keeps answering, as a
607
+ * GET alias, with a named project carried along. */
591
608
  async function showDone(ctx) {
592
- const { response, project } = ctx;
593
- return sendScreen(response, 200, donePage(chromeFor(project, "runs"), store.listCompletedWorkScoped(project, 50), pr => store.ciFailureObserved(pr)));
609
+ const { url, response } = ctx;
610
+ const named = url.searchParams.get("project");
611
+ return redirectGet(response, `/work?view=completed${named === null || named === "" ? "" : `&project=${encodeURIComponent(named)}`}`);
594
612
  }
595
613
  /** The old task list, retired into /work (docs/plans/reui-polish.md slice 7): its address keeps answering, as a
596
614
  * redirect, so links and bookmarks still land. `state` carries over (an unknown one is still refused); `template`
@@ -624,7 +642,9 @@ export function createTasksHandlers(runtime) {
624
642
  const prefill = picked !== null && picked.kind === "task"
625
643
  ? { title: picked.title, goal: picked.goal, not: picked.outOfScope ?? "", touches: picked.touches.join(", "), acceptance: acceptanceToLines(picked.acceptance).join("\n") }
626
644
  : null;
627
- return newTaskScreen(ctx, 200, null, prefill);
645
+ // An idea's link (or any ?text=) puts words in the box: bounded to what the box takes, nothing filed.
646
+ const text = ctx.url.searchParams.get("text");
647
+ return newTaskScreen(ctx, 200, null, prefill, text === null ? null : text.slice(0, 200));
628
648
  }
629
649
  async function showTask(ctx) {
630
650
  const { url, who, response } = ctx;
@@ -647,7 +667,8 @@ export function createTasksHandlers(runtime) {
647
667
  // One page per task (docs/plans/one-page-task-and-agent.md): the tab named, and the Result tab's result — the shown
648
668
  // version's own, or the exact run `run=` names, which must be a result of that very version.
649
669
  const tabWord = url.searchParams.get("tab");
650
- const tab = tabWord === "result" || tabWord === "plan" ? tabWord : "activity";
670
+ // The first tab (Plan, or Result once there is one) unless the address names Activity or Attempts.
671
+ const tab = tabWord === "result" || tabWord === "activity" || tabWord === "attempts" ? tabWord : "plan";
651
672
  const runWord = url.searchParams.get("run");
652
673
  const named = runWord === null ? null : /^[1-9][0-9]{0,14}$/.test(runWord) ? Number(runWord) : -1;
653
674
  if (named === -1)
@@ -695,22 +716,12 @@ export function createTasksHandlers(runtime) {
695
716
  "cache-control": "no-store", "x-content-type-options": "nosniff" });
696
717
  return void response.end(JSON.stringify(checkResponse("evidencePack", pack), null, 2));
697
718
  }
698
- return sendScreen(response, 200, screen(`Evidence pack · ${family.root.id}`, evidencePackHtml(pack), { chrome: chromeFor(family.root.repo, "tasks") }));
719
+ return sendScreen(response, 200, evidenceRecordPage(chromeFor(family.root.repo, "tasks"), pack));
699
720
  }
721
+ /** Build history lives on Agents now ("Finished in the last …"), and each task keeps its own attempts: the old address
722
+ * keeps answering, as a GET alias. */
700
723
  async function showRuns(ctx) {
701
- const { url, who, response, project } = ctx;
702
- const raw = url.searchParams.get("before");
703
- let before = null;
704
- if (raw !== null) {
705
- if (!/^[1-9][0-9]{0,14}$/.test(raw) || !Number.isSafeInteger(Number(raw))) {
706
- return refuse(response, who, 400, "that cursor is not a page", "/runs");
707
- }
708
- before = Number(raw);
709
- }
710
- const rows = store.listRunsBefore(before, RUNS_PAGE, project);
711
- const verdicts = store.proofVerdictsFor(rows.map(one => one.id));
712
- const accepted = new Set(rows.filter(one => store.proofAcceptance(one.id) !== null).map(one => one.id));
713
- return sendScreen(response, 200, runsPage(chromeFor(project, "runs"), rows, liveRunIds(rows), rows.length === RUNS_PAGE ? rows[rows.length - 1]?.id ?? null : null, verdicts, accepted));
724
+ return redirectGet(ctx.response, "/agents");
714
725
  }
715
726
  async function showRun(ctx) {
716
727
  const { url, who, response, now, project } = ctx;
@@ -873,21 +884,12 @@ export function createTasksHandlers(runtime) {
873
884
  return redirect(ctx.response, "/flows");
874
885
  }
875
886
  async function showDecision(ctx) {
876
- const { url, who, response, now, project } = ctx;
887
+ const { url, who, response, now } = ctx;
877
888
  const one = /^\/d\/([0-9]{1,15})$/.exec(url.pathname);
878
889
  if (one === null)
879
890
  return noPage(who, response);
880
- const decision = store.getDecision(Number(one[1]));
881
- if (decision === null)
882
- return refuse(response, who, 404, "no such decision");
883
- const run = store.getRun(decision.run);
884
- if (run !== null && !visible(taskRepoOf(run.taskRef))) {
885
- return refuse(response, who, 404, "no such decision");
886
- }
887
- const taskId = taskOf(store, decision);
888
891
  const decisionReturn = url.searchParams.get("return");
889
- const back = decisionReturn === null ? null : safeChatReturn(decisionReturn);
890
- return sendScreen(response, 200, decisionPage(chromeFor(project, "none"), decision, taskId, store.evidenceFor(decision.id), who, now, back));
892
+ return decisionScreen(ctx, Number(one[1]), decisionReturn === null ? null : safeChatReturn(decisionReturn), null, 200, now, null);
891
893
  }
892
894
  async function showDecisionEvidence(ctx) {
893
895
  const { url, who, response } = ctx;
@@ -1252,7 +1254,12 @@ export function createTasksHandlers(runtime) {
1252
1254
  // Irreversible options never ride one accidental tap: the form arms
1253
1255
  // them behind an explicit confirmation field, and the server checks —
1254
1256
  // the client rendering is convenience, this is the rule.
1257
+ // The question page's own form hears a refusal in its box, with nothing changed; /next keeps its refusal page.
1258
+ const inPlace = requestedReturn !== "next";
1259
+ const pageBack = requestedReturn === null ? null : decisionBack;
1255
1260
  if (chosen !== undefined && !chosen.reversible && body.get("confirm") !== "yes") {
1261
+ if (inPlace)
1262
+ return decisionScreen(ctx, id, pageBack, `“${chosen.label}” can't be undone: tick the box to confirm it`, 400, now, { choice, note: body.get("note") });
1256
1263
  return refuse(response, who, 400, "an irreversible choice must be confirmed", requestedReturn === null ? `/d/${id}` : `/d/${id}?return=${encodeURIComponent(decisionBack)}`);
1257
1264
  }
1258
1265
  const note = body.get("note");
@@ -1267,6 +1274,9 @@ export function createTasksHandlers(runtime) {
1267
1274
  const status = answered.reason === "bad-option" || answered.reason === "bad-note" ? 400 : 409;
1268
1275
  const closed = answered.reason === "closed" ? store.getDecision(id)?.supersededReason ?? null : null;
1269
1276
  const why = closed !== null ? closedQuestionWords(closed) : answered.reason === "already-answered" ? "already answered — somebody got there first" : answered.reason;
1277
+ if (inPlace && answered.reason !== "closed" && answered.reason !== "already-answered") {
1278
+ return decisionScreen(ctx, id, pageBack, answered.reason === "bad-option" ? "Choose one of the options" : answered.reason === "bad-note" ? `The note is too long or can't be sent (up to ${LIMITS.note} characters)` : why, status, now, { choice, note });
1279
+ }
1270
1280
  return refuse(response, who, status, why, requestedReturn === null ? `/d/${id}` : decisionBack);
1271
1281
  }
1272
1282
  return redirect(response, decisionBack);
@@ -3060,6 +3070,21 @@ export function createTasksHandlers(runtime) {
3060
3070
  return live;
3061
3071
  }
3062
3072
  const tidyHandlers = createTidyHandlers(runtime);
3073
+ /** The question page, with a refusal in its box when an answer was refused (the box keeps the choice to make). */
3074
+ function decisionScreen(ctx, id, back, refusal, code, now, draft) {
3075
+ const { who, response, project } = ctx;
3076
+ const decision = store.getDecision(id);
3077
+ if (decision === null)
3078
+ return refuse(response, who, 404, "no such decision");
3079
+ const run = store.getRun(decision.run);
3080
+ if (run !== null && !visible(taskRepoOf(run.taskRef))) {
3081
+ return refuse(response, who, 404, "no such decision");
3082
+ }
3083
+ const taskId = taskOf(store, decision);
3084
+ const ref = taskId === "?" ? null : store.lookupRef(taskId);
3085
+ const place = { title: ref === null ? null : store.getTask(taskId)?.title ?? null, repo: ref?.repo ?? null, run: run === null ? null : { id: run.id, role: run.role } };
3086
+ return sendScreen(response, code, decisionPage(chromeFor(project, "none"), decision, taskId, store.evidenceFor(decision.id), now, place, back, refusal, draft));
3087
+ }
3063
3088
  const registrations = handlersOf("tasks", {
3064
3089
  ...tidyHandlers,
3065
3090
  "code.page": showCode("workspace"),
@@ -3070,12 +3095,13 @@ export function createTasksHandlers(runtime) {
3070
3095
  "code.changes-read": showCode("changes", true),
3071
3096
  "code.ship-preview": showCode("ship", true),
3072
3097
  "code.other": showCode("unknown"),
3098
+ // Retired into Tasks and the task page (src/design/KIT.md): each old address still answers, as a redirect.
3073
3099
  "inbox": showInbox,
3074
3100
  "work": showWork,
3075
3101
  "next": showNext,
3076
3102
  "board": showBoard,
3077
3103
  "review": showReview,
3078
- "done": showDone,
3104
+ "done": async (ctx) => { const to = retiredTasksAddress("done", ctx.url); return to === null ? showDone(ctx) : redirect(ctx.response, to); },
3079
3105
  "tasks": showTasks,
3080
3106
  "queue": showQueue,
3081
3107
  "tasks.new": showNewTask,
@@ -3142,7 +3168,7 @@ export function createTasksHandlers(runtime) {
3142
3168
  });
3143
3169
  /** The add page, once more: blank, from a shipped template (`?template=`, as the old task list did), or with a refused
3144
3170
  * filing's own words and status (the draft keeps the project revision it was written against). */
3145
- function newTaskScreen(ctx, status, problem, draft) {
3171
+ function newTaskScreen(ctx, status, problem, draft, text = null) {
3146
3172
  const { who, response, project } = ctx;
3147
3173
  const chainable = store
3148
3174
  .listTasksScoped(project, undefined, 100, null)
@@ -3154,7 +3180,12 @@ export function createTasksHandlers(runtime) {
3154
3180
  chainable.unshift({ id: after, title: "(as submitted)" });
3155
3181
  const chrome = chromeFor(project, "tasks");
3156
3182
  const revision = draft?.values !== undefined ? undefined : who.via === "cookie" ? who.session.projectRevision : 0;
3157
- return sendScreen(response, status, newTaskPage(chrome, project, revision, problem, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? [], draft));
3183
+ // Ideas for the project the box starts on: what the first run found there (issues, TODOs), else the safe generic
3184
+ // ones while the first run is open; none once someone has filed work and nothing was found.
3185
+ const startsOn = draft?.values?.get("repo") ?? project ?? chrome.projects?.[0]?.path ?? null;
3186
+ const found = startsOn === null ? null : runtime.firstTasks.get(startsOn)?.found ?? null;
3187
+ const ideas = found ?? (runtime.firstRunStepsNow(ctx.now) !== null && !store.hasAnyWork() ? firstTaskSuggestions({ issues: [], todos: [] }) : []);
3188
+ return sendScreen(response, status, newTaskPage(chrome, project, revision, problem, chainable, store.permissionDefault().mode, store.qualityDefault().mode, chrome.projects ?? [], draft, text, ideas));
3158
3189
  }
3159
3190
  return { registrations, runListPane, peekFragment, taskMutation, queueRegionFor, ciFailingFor, completedRowFor, attemptRowFor, resultRowOf, reviewCockpitViewOf, decisionEvidence, runEvidence, sendArtifact, peekGuards, liveRunIds };
3160
3191
  }
@@ -3206,3 +3237,27 @@ export function createTidyHandlers(runtime) {
3206
3237
  };
3207
3238
  return { "work.put-away": showPutAway, "work.put-away.apply": applyPutAway, "task.act.archive": oneTask("archive"), "task.act.restore": oneTask("restore") };
3208
3239
  }
3240
+ /** A retired page's GET alias: a plain 302 (the shared 303 helper belongs to POST landings), as pages.ts morningAlias. */
3241
+ function redirectGet(response, to) {
3242
+ response.writeHead(302, { ...SAFETY, Location: to });
3243
+ response.end();
3244
+ }
3245
+ /**
3246
+ * Where a retired Tasks address goes now (wave A, src/design/KIT.md): Done is Finished. `board` names where the old
3247
+ * Board's state view goes once it is retired (Tasks' Board view; its Order view and its live fragment stay). Null: this
3248
+ * address still answers itself.
3249
+ */
3250
+ export function retiredTasksAddress(route, url) {
3251
+ // (The Board keeps its own page for now: Tasks' Board view is where the app links; see the wave A report.)
3252
+ const project = url.searchParams.get("project");
3253
+ const keep = project === null || url.searchParams.getAll("project").length !== 1 ? "" : `project=${encodeURIComponent(project)}&`;
3254
+ switch (route) {
3255
+ case "done": return `/work?${keep}view=completed`;
3256
+ case "board":
3257
+ if (url.searchParams.get("view") === "order" || url.searchParams.get("fragment") === "1")
3258
+ return null;
3259
+ return url.searchParams.get("scope") === "all" ? "/work?all=1&layout=board" : `/work?${keep}layout=board`;
3260
+ }
3261
+ }
3262
+ /** How many cards a Board column shows; the column's count covers them all and its "View all" opens the list. */
3263
+ export const BOARD_COLUMN_LIMIT = 40;
@@ -78,7 +78,7 @@ export function createTerminalHandlers(runtime, deps) {
78
78
  const id = TERMINAL_ID.exec(url.pathname)?.[1] ?? "";
79
79
  const nonce = randomBytes(16).toString("base64");
80
80
  const host = request.headers.host ?? "";
81
- const document = html `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content"><meta name="theme-color" content="#0C0E12"><meta name="apple-mobile-web-app-capable" content="yes"><title>Live — Toolroll</title><link rel="icon" href="/icon.svg"><link rel="manifest" href="/manifest.webmanifest"><link rel="stylesheet" href="/assets/terminal.css"></head><body><div id="so-term"></div><div id="so-term-boot" hidden data-csrf="${who.session.csrf}" data-session="${id}" data-passwordless="${passwordlessOn(store) ? "1" : ""}"></div><script type="module" src="/assets/terminal.js" nonce="${nonce}"></script></body></html>`;
81
+ const document = html `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content"><meta name="theme-color" content="#0C0E12"><meta name="apple-mobile-web-app-capable" content="yes"><title>Terminals — Toolroll</title><link rel="icon" href="/icon.svg"><link rel="manifest" href="/manifest.webmanifest"><link rel="stylesheet" href="/assets/terminal.css"></head><body><div id="so-term"></div><div id="so-term-boot" hidden data-csrf="${who.session.csrf}" data-session="${id}" data-passwordless="${passwordlessOn(store) ? "1" : ""}"></div><script type="module" src="/assets/terminal.js" nonce="${nonce}"></script></body></html>`;
82
82
  response.writeHead(200, {
83
83
  "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", "Referrer-Policy": "no-referrer",
84
84
  // The socket is this page's own host: named outright, since older WebKit doesn't read 'self' as ws:/wss:.
@@ -0,0 +1,22 @@
1
+ import { type FirstRunStep } from "../first-run.js";
2
+ import { type CheckStep } from "./kit.js";
3
+ import type { ServerRuntime } from "./runtime.js";
4
+ import { type Chrome, type Screen } from "./chrome.js";
5
+ export type WelcomeFacts = {
6
+ steps: readonly FirstRunStep[];
7
+ projects: number;
8
+ /** This person has a live push enrollment on some device. */
9
+ alerts: boolean;
10
+ };
11
+ /** The five steps as the page shows them, and which one leads. */
12
+ export declare function welcomeSteps(facts: WelcomeFacts): {
13
+ steps: CheckStep[];
14
+ primary: {
15
+ label: string;
16
+ href: string;
17
+ } | null;
18
+ };
19
+ export declare function welcomePage(chrome: Chrome, facts: WelcomeFacts): Screen;
20
+ export declare function createWelcomeHandlers(runtime: ServerRuntime): {
21
+ registrations: import("./handler-registry.js").Registration[];
22
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The first run's own page, /welcome (Wave E, src/design/KIT.md): five guided steps to a first result, each derived
3
+ * from live facts on every render (never a stored cursor), each with the one action that does it. The next required
4
+ * step's action is the screen's one primary (on a fresh install: Add a project); alerts on the phone are optional and
5
+ * never lead. It only reads: every step's action is a link to the page that does it. Once the first result has
6
+ * arrived (or for a project-limited account, or in the demo) the first run is over and the address leads Home.
7
+ */
8
+ import { builderReadyFor, firstRunBuilderRepo } from "../dispatch.js";
9
+ import { withBuilderStep } from "../first-run.js";
10
+ import { html } from "../html.js";
11
+ import { START_BUILDER_HREF } from "../needs-you.js";
12
+ import { handlersOf } from "./handler-registry.js";
13
+ import { redirect } from "./http.js";
14
+ import { btn, checklist, dock, pageHeader } from "./kit.js";
15
+ import { screen } from "./chrome.js";
16
+ const plural = (n, one, many) => `${n} ${n === 1 ? one : many}`;
17
+ /** The five steps as the page shows them, and which one leads. */
18
+ export function welcomeSteps(facts) {
19
+ const step = (key) => facts.steps.find(one => one.key === key);
20
+ const agent = step("agent"), builder = step("builder"), task = step("task");
21
+ const projectDone = facts.projects > 0;
22
+ const agentDone = (agent?.done ?? false) && (builder?.done ?? true);
23
+ const command = agent?.action?.kind === "command" ? agent.action.command : null;
24
+ const agentState = agentDone ? "done" : agent?.checking === true ? "checking" : "open";
25
+ const agentText = agentDone ? "An agent is signed in and a builder is ready."
26
+ : agent?.checking === true ? "Checking for a signed-in agent on this computer…"
27
+ : agent !== undefined && !agent.done && command !== null ? html `On the computer running Toolroll, run <code class="k-mono">${command}</code>.`
28
+ : "Start a builder so tasks can run.";
29
+ const agentLink = agentDone || agent?.checking === true ? null
30
+ : agent !== undefined && !agent.done ? { label: "Agent settings", href: "/settings/models" } : { label: "Start a builder", href: START_BUILDER_HREF };
31
+ const taskDone = task?.done ?? false;
32
+ // The next required step leads (alerts are optional and never do).
33
+ const order = [
34
+ { key: "project", open: !projectDone, link: { label: "Add a project", href: "/projects" } },
35
+ { key: "agent", open: agentState === "open", link: agentLink },
36
+ { key: "task", open: !taskDone && projectDone, link: { label: "New task", href: "/tasks/new" } },
37
+ ];
38
+ const lead = order.find(one => one.open && one.link !== null) ?? null;
39
+ const action = (key, link) => link === null ? null : btn(link.label, { sm: true, href: link.href, ...(lead?.key === key ? { variant: "primary", docked: true } : {}) });
40
+ const steps = [
41
+ { key: "password", title: "Your password", text: "You're signed in. Your first login is saved on the computer running Toolroll.", state: "done" },
42
+ { key: "project", title: "Add a project", text: projectDone ? plural(facts.projects, "project added.", "projects added.") : "A git repository agents work in.", state: projectDone ? "done" : "open", action: action("project", order[0].link) },
43
+ { key: "agent", title: "Agent and models", text: agentText, state: agentState, action: action("agent", agentLink) },
44
+ { key: "phone", title: "Alerts on your phone", text: facts.alerts ? "Alerts are on." : "Optional. Hear when something needs you.", state: facts.alerts ? "done" : "open", action: btn("Set up alerts", { sm: true, href: "/settings/alerts#this-device" }) },
45
+ { key: "task", title: "Your first task", text: taskDone ? "Filed. Its plan waits for your approval." : "Describe a change and approve its plan.", state: taskDone ? "done" : "open",
46
+ action: projectDone ? action("task", order[2].link) : null },
47
+ ];
48
+ return { steps, primary: lead?.link ?? null };
49
+ }
50
+ export function welcomePage(chrome, facts) {
51
+ const { steps, primary } = welcomeSteps(facts);
52
+ const left = steps.filter(one => one.state !== "done").length;
53
+ const body = html `${pageHeader("Welcome")}<div class="k-doc"><div class="k-hello"><h2>Get to your first result.</h2><p>${left === 0 ? "Setup is done." : `${plural(left, "step", "steps")} left.`}</p></div>\
54
+ ${checklist(steps, "Setup steps")}</div>${dock(primary === null ? null : btn(primary.label, { variant: "primary", href: primary.href }))}`;
55
+ return screen("Welcome", body, { chrome });
56
+ }
57
+ export function createWelcomeHandlers(runtime) {
58
+ const { store, sendScreen, chromeFor, firstRunStepsNow, managedRepos, visible } = runtime;
59
+ async function showWelcome(ctx) {
60
+ const { who, response, now, project } = ctx;
61
+ const steps = withBuilderStep(firstRunStepsNow(now), builderReadyFor(store, firstRunBuilderRepo(store, project, visible, managedRepos()), now));
62
+ // The first run is over (a first result, a project-limited account, the demo): the address leads Home.
63
+ if (steps === null)
64
+ return redirect(response, "/");
65
+ const alerts = who.via === "cookie" && store.listPushSubscriptions(who.name).some(one => one.retiredAt === null);
66
+ return sendScreen(response, 200, welcomePage(chromeFor(project, "work"), { steps, projects: managedRepos().length, alerts }));
67
+ }
68
+ return { registrations: handlersOf("tasks", { welcome: showWelcome }) };
69
+ }