toolroll 0.9.85 → 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 (220) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/dist/agent-fence.d.ts +6 -2
  3. package/dist/agent-fence.js +3 -2
  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/assignment.js +4 -3
  10. package/dist/backup-ui.d.ts +5 -4
  11. package/dist/backup-ui.js +65 -32
  12. package/dist/browser/terminal.css +1 -1
  13. package/dist/browser/terminal.js +57 -54
  14. package/dist/browser/workspace.css +2 -2
  15. package/dist/browser/workspace.js +35 -68
  16. package/dist/browser-crew.js +1 -1
  17. package/dist/browser-workspace.d.ts +363 -177
  18. package/dist/browser-workspace.js +10 -9
  19. package/dist/chat-actions.d.ts +11 -1
  20. package/dist/chat-actions.js +53 -1
  21. package/dist/chat-channel.js +1 -0
  22. package/dist/chat-controls.d.ts +1 -1
  23. package/dist/chat-controls.js +1 -1
  24. package/dist/chat-quiet.js +3 -2
  25. package/dist/chat-settings-page.d.ts +29 -9
  26. package/dist/chat-settings-page.js +59 -28
  27. package/dist/check-levels-ui.d.ts +2 -1
  28. package/dist/check-levels-ui.js +67 -58
  29. package/dist/claim.d.ts +3 -0
  30. package/dist/claim.js +4 -1
  31. package/dist/contracts/chat-actions.d.ts +9 -0
  32. package/dist/contracts/chat-actions.js +3 -0
  33. package/dist/contracts/cli.d.ts +43 -0
  34. package/dist/contracts/cli.js +4 -0
  35. package/dist/contracts/console-api.d.ts +4 -1
  36. package/dist/contracts/console-api.js +4 -1
  37. package/dist/contracts/flow-propose.d.ts +4 -1
  38. package/dist/contracts/flow-propose.js +1 -1
  39. package/dist/contracts/flow.d.ts +41 -0
  40. package/dist/contracts/flow.js +4 -2
  41. package/dist/contracts/lead-tools.d.ts +21 -0
  42. package/dist/contracts/lead-tools.js +2 -0
  43. package/dist/contracts/store-json.d.ts +2 -0
  44. package/dist/contracts/trigger-payloads.d.ts +20 -0
  45. package/dist/contracts/trigger-payloads.js +4 -0
  46. package/dist/coordinator.js +1 -0
  47. package/dist/credentials-ui.d.ts +5 -3
  48. package/dist/credentials-ui.js +58 -31
  49. package/dist/deploy-gate.js +4 -0
  50. package/dist/design/kit.css +685 -0
  51. package/dist/design/kit.d.ts +208 -0
  52. package/dist/design/kit.js +177 -0
  53. package/dist/design/tokens.css +19 -1
  54. package/dist/discord-settings.js +8 -4
  55. package/dist/dispatch.d.ts +3 -0
  56. package/dist/dispatch.js +4 -1
  57. package/dist/export-ui.d.ts +6 -4
  58. package/dist/export-ui.js +18 -8
  59. package/dist/flow-browser.d.ts +103 -0
  60. package/dist/flow-browser.js +204 -0
  61. package/dist/flow-changed.d.ts +14 -0
  62. package/dist/flow-changed.js +53 -0
  63. package/dist/flow-code.d.ts +5 -1
  64. package/dist/flow-code.js +4 -1
  65. package/dist/flow-engine.js +16 -0
  66. package/dist/flow-gallery-ui.d.ts +41 -4
  67. package/dist/flow-gallery-ui.js +95 -63
  68. package/dist/flow-live.js +9 -1
  69. package/dist/flow-pull-request.js +15 -7
  70. package/dist/flow-share.js +3 -1
  71. package/dist/flow-starters-ui.d.ts +3 -9
  72. package/dist/flow-starters-ui.js +12 -23
  73. package/dist/flow-steps.d.ts +9 -1
  74. package/dist/flow-steps.js +55 -5
  75. package/dist/flow-triggers.js +55 -0
  76. package/dist/flow-unattended-words.d.ts +20 -0
  77. package/dist/flow-unattended-words.js +24 -0
  78. package/dist/flow-unattended.d.ts +9 -5
  79. package/dist/flow-unattended.js +45 -23
  80. package/dist/flows-cli.d.ts +19 -0
  81. package/dist/flows-cli.js +54 -5
  82. package/dist/flows-ui.d.ts +12 -6
  83. package/dist/flows-ui.js +152 -32
  84. package/dist/flows.js +5 -2
  85. package/dist/integrations-ui.d.ts +6 -4
  86. package/dist/integrations-ui.js +34 -40
  87. package/dist/integrations.d.ts +2 -2
  88. package/dist/integrations.js +4 -4
  89. package/dist/kits-ui.d.ts +8 -10
  90. package/dist/kits-ui.js +29 -33
  91. package/dist/kits.js +1 -1
  92. package/dist/knowledge-ui.js +76 -19
  93. package/dist/lead-tools.js +35 -6
  94. package/dist/lead-voice.js +16 -5
  95. package/dist/ledger-view.d.ts +2 -0
  96. package/dist/ledger-view.js +41 -38
  97. package/dist/models-ui.d.ts +8 -3
  98. package/dist/models-ui.js +82 -59
  99. package/dist/monitoring-ui.d.ts +4 -2
  100. package/dist/monitoring-ui.js +44 -13
  101. package/dist/needs-you.d.ts +1 -1
  102. package/dist/needs-you.js +8 -0
  103. package/dist/operate-remote-arguments.js +1 -0
  104. package/dist/operate-remote.js +1 -1
  105. package/dist/operate.d.ts +1 -1
  106. package/dist/operate.js +249 -6
  107. package/dist/passwordless.d.ts +1 -1
  108. package/dist/passwordless.js +1 -1
  109. package/dist/people-audit-ui.d.ts +2 -1
  110. package/dist/people-audit-ui.js +25 -23
  111. package/dist/policy-ui.d.ts +3 -2
  112. package/dist/policy-ui.js +27 -21
  113. package/dist/policy.d.ts +3 -0
  114. package/dist/policy.js +14 -1
  115. package/dist/project-delete-ui.d.ts +4 -2
  116. package/dist/project-delete-ui.js +135 -126
  117. package/dist/project-delete.js +7 -3
  118. package/dist/project-knowledge.d.ts +10 -10
  119. package/dist/publish.d.ts +11 -0
  120. package/dist/publish.js +16 -3
  121. package/dist/pull-request-flow.js +35 -4
  122. package/dist/pull-request-ui.d.ts +2 -1
  123. package/dist/pull-request-ui.js +39 -28
  124. package/dist/recipe-ui.d.ts +4 -2
  125. package/dist/recipe-ui.js +14 -4
  126. package/dist/release-adapters.d.ts +3 -0
  127. package/dist/release-adapters.js +22 -2
  128. package/dist/release-cli.js +81 -9
  129. package/dist/release.d.ts +21 -0
  130. package/dist/release.js +40 -4
  131. package/dist/releases.js +1 -1
  132. package/dist/request-budget-ui.d.ts +4 -2
  133. package/dist/request-budget-ui.js +30 -19
  134. package/dist/result-follow-ups.js +4 -0
  135. package/dist/retention-ui.d.ts +4 -3
  136. package/dist/retention-ui.js +15 -12
  137. package/dist/scope.d.ts +8 -0
  138. package/dist/scope.js +13 -0
  139. package/dist/serve.d.ts +0 -1
  140. package/dist/serve.js +51 -7
  141. package/dist/server/agents-computers.d.ts +9 -0
  142. package/dist/server/agents-computers.js +67 -0
  143. package/dist/server/chat.js +20 -18
  144. package/dist/server/chrome.d.ts +1 -1
  145. package/dist/server/chrome.js +19 -9
  146. package/dist/server/console.css +31 -2
  147. package/dist/server/flows.js +79 -36
  148. package/dist/server/home.d.ts +35 -0
  149. package/dist/server/home.js +104 -0
  150. package/dist/server/http.d.ts +3 -0
  151. package/dist/server/http.js +5 -0
  152. package/dist/server/kit-standalone.d.ts +22 -0
  153. package/dist/server/kit-standalone.js +69 -0
  154. package/dist/server/kit.d.ts +405 -0
  155. package/dist/server/kit.js +361 -0
  156. package/dist/server/pages.d.ts +2 -1
  157. package/dist/server/pages.js +213 -174
  158. package/dist/server/people-tokens.js +26 -85
  159. package/dist/server/render-history.d.ts +51 -0
  160. package/dist/server/render-history.js +228 -0
  161. package/dist/server/render-new-task.d.ts +29 -0
  162. package/dist/server/render-new-task.js +96 -0
  163. package/dist/server/render-next.d.ts +63 -0
  164. package/dist/server/render-next.js +109 -0
  165. package/dist/server/render-pages.d.ts +35 -190
  166. package/dist/server/render-pages.js +154 -563
  167. package/dist/server/render-people.d.ts +24 -3
  168. package/dist/server/render-people.js +93 -72
  169. package/dist/server/render-projects.d.ts +103 -0
  170. package/dist/server/render-projects.js +247 -0
  171. package/dist/server/render-results.d.ts +5 -24
  172. package/dist/server/render-results.js +48 -185
  173. package/dist/server/render-settings.d.ts +5 -74
  174. package/dist/server/render-settings.js +102 -493
  175. package/dist/server/render-tasks.d.ts +43 -5
  176. package/dist/server/render-tasks.js +131 -91
  177. package/dist/server/route-table.js +31 -5
  178. package/dist/server/settings.js +276 -255
  179. package/dist/server/task-views.d.ts +8 -0
  180. package/dist/server/task-views.js +90 -2
  181. package/dist/server/tasks.d.ts +18 -0
  182. package/dist/server/tasks.js +148 -41
  183. package/dist/server/terminals.js +1 -1
  184. package/dist/server/welcome.d.ts +22 -0
  185. package/dist/server/welcome.js +69 -0
  186. package/dist/settings-groups-ui.d.ts +279 -0
  187. package/dist/settings-groups-ui.js +615 -0
  188. package/dist/settings-kit.d.ts +42 -6
  189. package/dist/settings-kit.js +69 -7
  190. package/dist/settings-proofs.js +1 -0
  191. package/dist/setup-guide.d.ts +2 -2
  192. package/dist/skills-ui.js +59 -7
  193. package/dist/slack-settings.js +12 -5
  194. package/dist/spend-ui.d.ts +45 -26
  195. package/dist/spend-ui.js +124 -56
  196. package/dist/sso-ui.d.ts +5 -2
  197. package/dist/sso-ui.js +63 -31
  198. package/dist/storage-ui.d.ts +3 -2
  199. package/dist/storage-ui.js +56 -50
  200. package/dist/store.d.ts +182 -5
  201. package/dist/store.js +512 -38
  202. package/dist/surface.js +23 -3
  203. package/dist/task-archive.d.ts +10 -0
  204. package/dist/task-archive.js +16 -0
  205. package/dist/task-control.d.ts +1 -1
  206. package/dist/teams-settings.d.ts +2 -1
  207. package/dist/teams-settings.js +24 -41
  208. package/dist/telegram-settings.d.ts +0 -4
  209. package/dist/telegram-settings.js +52 -25
  210. package/dist/tidy.d.ts +130 -0
  211. package/dist/tidy.js +503 -0
  212. package/dist/toolroll-update-ui.d.ts +8 -4
  213. package/dist/toolroll-update-ui.js +55 -45
  214. package/dist/tools-ui.d.ts +5 -3
  215. package/dist/tools-ui.js +107 -42
  216. package/dist/work-index.d.ts +24 -0
  217. package/dist/work-index.js +56 -30
  218. package/dist/workspace-ui.d.ts +1 -1
  219. package/dist/workspace-ui.js +8 -4
  220. package/package.json +1 -1
@@ -38,7 +38,7 @@ import { WORK_INDEX_MAX_LIMIT, workIndexPage } from "../work-index.js";
38
38
  import { html, htmlString, postForm, reauth } from "../html.js";
39
39
  import { requestContext } from "./request-context.js";
40
40
  import { refuse, screen, whenTime } from "./chrome.js";
41
- import { chatResultHref, chatReturnWithLatest, chatReturnWithSaid, projectChatHref, redirect, respond, safeChatReturn, safeReturn, taskChatHref, taskHref } from "./http.js";
41
+ import { chatResultHref, chatReturnWithLatest, chatRefusal, chatReturnWithSaid, projectChatHref, redirect, respond, safeChatReturn, safeReturn, taskChatHref, taskHref } from "./http.js";
42
42
  import { chatAckPage, chatPage, NOT_STARTED_SESSION, coordinatorProposalsSection, decisionsFor, leadAfterComposerHtml, leadChatVersion, leadMintCard, leadPage, leadThreadHtml, PHONE_CARD_FACT, TASK_COMPOSER_MODES, taskChatLiveRegion, teamProposalCardParts } from "./render-chat.js";
43
43
  import {} from "./render-pages.js";
44
44
  import { completionForm, hardBlockerOf, homePhaseWords, owedAcceptanceOf, resultPanelHtml } from "./render-results.js";
@@ -389,9 +389,11 @@ export function createChatHandlers(runtime) {
389
389
  const id = proposalId, saved = store.getLeadProposal(id), savedAction = saved?.kind === 'action' ? sharedActionPayload(saved.payload) : null;
390
390
  if (saved && savedAction && saved.state !== 'pending' && (sharedConversation !== null || store.getLeadThread(saved.thread)?.approver === principal.name) && principal.repos.includes(savedAction.repo) && store.accountCanAccess(principal.name, savedAction.repo)) {
391
391
  const task = typeof saved.outcome?.['taskId'] === 'string' ? saved.outcome['taskId'] : typeof savedAction.request['task'] === 'string' ? savedAction.request['task'] : null;
392
- const destination = task ? taskHref(task) : `/settings/${savedAction.operation.startsWith('skill_') ? 'skills' : 'knowledge'}?repo=${encodeURIComponent(savedAction.repo)}`;
392
+ // A flow action leads back to its flow (a page path of our own making, never the proposal's words).
393
+ const flowId = savedAction.operation.startsWith('flow_') && Number.isSafeInteger(savedAction.request['flow']) ? Number(savedAction.request['flow']) : null;
394
+ const destination = task ? taskHref(task) : flowId !== null ? `/flows/${flowId}` : `/settings/${savedAction.operation.startsWith('skill_') ? 'skills' : 'knowledge'}?repo=${encodeURIComponent(savedAction.repo)}`;
393
395
  const said = typeof saved.outcome?.['said'] === 'string' ? saved.outcome['said'] : saved.state === 'dismissed' ? 'Action dismissed.' : saved.state === 'expired' ? 'This proposal expired. Ask for a fresh proposal.' : 'This action has no completed outcome yet.';
394
- return sendScreen(response, 200, screen('Action outcome', html `<h1>${savedAction.title}</h1><p>${said}</p><a class="button-link" href="${destination}">${task ? 'Open task' : 'Open project settings'}</a>`, { chrome: chromeFor(savedAction.repo, 'chat') }));
396
+ return sendScreen(response, 200, screen('Action outcome', html `<h1>${savedAction.title}</h1><p>${said}</p><a class="button-link" href="${destination}">${task ? 'Open task' : flowId !== null ? 'Open flow' : 'Open project settings'}</a>`, { chrome: chromeFor(savedAction.repo, 'chat') }));
395
397
  }
396
398
  const review = mintSharedActionReview(store, principal, id, evidenceRoot, clock()), action = review.payload;
397
399
  const terms = action.terms.map(term => html `<p style="white-space:pre-wrap;overflow-wrap:anywhere">${term}</p>`);
@@ -525,18 +527,18 @@ export function createChatHandlers(runtime) {
525
527
  return refuse(response, who, 403, "the sandbox never pushes");
526
528
  const password = body.get("token") ?? "";
527
529
  if (password === "" || !authenticateApprover(who, password).ok) {
528
- return redirect(response, `/settings?said=${encodeURIComponent("enrolling this device takes your password, typed again")}`);
530
+ return redirect(response, `/settings/alerts?said=${encodeURIComponent("enrolling this device takes your password, typed again")}#this-device`);
529
531
  }
530
532
  const endpoint = body.get("endpoint") ?? "";
531
533
  const p256dh = body.get("p256dh") ?? "";
532
534
  const auth = body.get("auth") ?? "";
533
535
  const checked = validatePushEndpoint(endpoint);
534
536
  if (!checked.ok)
535
- return redirect(response, `/settings?said=${encodeURIComponent(checked.problem)}`);
537
+ return redirect(response, `/settings/alerts?said=${encodeURIComponent(checked.problem)}#this-device`);
536
538
  const p256dhBytes = Buffer.from(p256dh, "base64url");
537
539
  const authBytes = Buffer.from(auth, "base64url");
538
540
  if (p256dhBytes.length !== 65 || p256dhBytes[0] !== 4 || authBytes.length !== 16) {
539
- return redirect(response, `/settings?said=${encodeURIComponent("that subscription's keys are not the shape a browser mints")}`);
541
+ return redirect(response, `/settings/alerts?said=${encodeURIComponent("that subscription's keys are not the shape a browser mints")}#this-device`);
540
542
  }
541
543
  const keys = loadOrCreateVapidKeys(dirname(options.telegramTokenFile ?? "."));
542
544
  const generation = store.approverGeneration(who.name) ?? 1;
@@ -550,9 +552,9 @@ export function createChatHandlers(runtime) {
550
552
  vapidFingerprint: keys.fingerprint,
551
553
  }, clock());
552
554
  if (!enrolled.ok) {
553
- return redirect(response, `/settings?said=${encodeURIComponent(enrolled.reason === "approver-cap" ? "five devices per person — remove one first" : "twenty devices per installation — remove one first")}`);
555
+ return redirect(response, `/settings/alerts?said=${encodeURIComponent(enrolled.reason === "approver-cap" ? "five devices per person — remove one first" : "twenty devices per installation — remove one first")}#this-device`);
554
556
  }
555
- return redirect(response, `/settings?said=${encodeURIComponent("this device now gets a buzz when the plane needs a person")}`);
557
+ return redirect(response, `/settings/alerts?said=${encodeURIComponent("this device now gets a buzz when the plane needs a person")}#this-device`);
556
558
  }
557
559
  async function pushRemove(ctx) {
558
560
  const { who, response, posted } = ctx;
@@ -562,9 +564,9 @@ export function createChatHandlers(runtime) {
562
564
  const id = Number(body.get("id") ?? "");
563
565
  const mine = store.listPushSubscriptions(who.name).find(one => one.id === id && one.retiredAt === null);
564
566
  if (mine === undefined)
565
- return redirect(response, `/settings?said=${encodeURIComponent("that device is not yours to remove, or it is already gone")}`);
567
+ return redirect(response, `/settings/alerts?said=${encodeURIComponent("that device is not yours to remove, or it is already gone")}#this-device`);
566
568
  store.retirePushSubscription(mine.id, `removed by ${who.name}`, clock());
567
- return redirect(response, `/settings?said=${encodeURIComponent("removed — that device stops receiving pushes")}`);
569
+ return redirect(response, `/settings/alerts?said=${encodeURIComponent("removed — that device stops receiving pushes")}#this-device`);
568
570
  }
569
571
  async function phoneDismiss(ctx) {
570
572
  const { who, response, now, posted } = ctx;
@@ -585,7 +587,7 @@ export function createChatHandlers(runtime) {
585
587
  const back = body.get("return") === "/settings/lead" ? "/settings/lead" : safeChatReturn(body.get("return"));
586
588
  const password = body.get("token") ?? "";
587
589
  if (password === "" || !authenticateApprover(who, password).ok) {
588
- return redirect(response, chatReturnWithSaid(back, "configuring chat spend takes your password, typed again"));
590
+ return redirect(response, chatRefusal(back, "configuring chat spend takes your password, typed again"));
589
591
  }
590
592
  if (body.get("off") === "1") {
591
593
  store.clearChatConfig();
@@ -602,12 +604,12 @@ export function createChatHandlers(runtime) {
602
604
  const weekly = Number(weeklyText);
603
605
  const daily = (body.get("daily-turns") ?? "").trim() === "" ? 50 : Number(body.get("daily-turns"));
604
606
  if (provider !== "anthropic-api" && provider !== "openrouter-api" && provider !== "claude-subscription" && provider !== "codex-subscription") {
605
- return redirect(response, chatReturnWithSaid(back, "pick a chat provider"));
607
+ return redirect(response, chatRefusal(back, "pick a chat provider"));
606
608
  }
607
609
  const subscription = isSubscriptionChatProvider(provider);
608
610
  const model = requestedModel === "" && subscription ? "default" : requestedModel;
609
611
  if (!validModelId(model)) {
610
- return redirect(response, chatReturnWithSaid(back, "the model id must be 1–128 letters, digits, dots, slashes, colons, underscores, or dashes"));
612
+ return redirect(response, chatRefusal(back, "the model id must be 1–128 letters, digits, dots, slashes, colons, underscores, or dashes"));
611
613
  }
612
614
  // The key, when pasted, is stored FIRST (0600 file, Telegram-token
613
615
  // precedent) so the catalog fetch below can already use it. It never
@@ -615,10 +617,10 @@ export function createChatHandlers(runtime) {
615
617
  const pastedKey = (body.get("key") ?? "").trim();
616
618
  if (pastedKey !== "") {
617
619
  if (subscription)
618
- return redirect(response, chatReturnWithSaid(back, "subscription chat uses the CLI's cached login — do not paste an API key"));
620
+ return redirect(response, chatRefusal(back, "subscription chat uses the CLI's cached login — do not paste an API key"));
619
621
  const stored = storeChatKey(provider, pastedKey);
620
622
  if (!stored.ok)
621
- return redirect(response, chatReturnWithSaid(back, stored.message));
623
+ return redirect(response, chatRefusal(back, stored.message));
622
624
  }
623
625
  // The pin: anthropic models come from the compiled table; openrouter
624
626
  // models come from OpenRouter's OWN catalog, priced by the authority
@@ -633,13 +635,13 @@ export function createChatHandlers(runtime) {
633
635
  pin = hit.price;
634
636
  }
635
637
  if (!subscription && pin === null) {
636
- return redirect(response, chatReturnWithSaid(back, provider === "openrouter-api" ? "that model is not in OpenRouter's catalog (or the catalog is unreachable) — chat cannot reserve spend it cannot bound" : "that model has no pinned price — chat cannot reserve spend it cannot bound"));
638
+ return redirect(response, chatRefusal(back, provider === "openrouter-api" ? "that model is not in OpenRouter's catalog (or the catalog is unreachable) — chat cannot reserve spend it cannot bound" : "that model has no pinned price — chat cannot reserve spend it cannot bound"));
637
639
  }
638
640
  if (!subscription && (!Number.isFinite(weekly) || weekly <= 0)) {
639
- return redirect(response, chatReturnWithSaid(back, "the weekly ceiling is a positive dollar amount — chat without one is unbounded, not configured"));
641
+ return redirect(response, chatRefusal(back, "the weekly ceiling is a positive dollar amount — chat without one is unbounded, not configured"));
640
642
  }
641
643
  if (!Number.isInteger(daily) || daily <= 0 || daily > 1_000) {
642
- return redirect(response, chatReturnWithSaid(back, "daily turns is a whole number between 1 and 1000"));
644
+ return redirect(response, chatRefusal(back, "daily turns is a whole number between 1 and 1000"));
643
645
  }
644
646
  store.setChatConfig({
645
647
  provider,
@@ -44,7 +44,7 @@ export declare const DEMO_BANNER_SHORT = "Demo: sample projects. Nothing calls a
44
44
  * React workspace, the pre-script fallback) reads these.
45
45
  *
46
46
  * The palette above, in light, dark and pinned themes, is Signal's generated
47
- * tokens.css (src/design, built by youi from the design-language repo); the
47
+ * tokens.css (src/design, generated by youiyoux from the toolroll System, src/design/system); the
48
48
  * console's own rules follow in console.css, shipped beside this module.
49
49
  * Their bytes are the served sheet's (WORKSPACE_STYLE hashes them). The React
50
50
  * workspace's stylesheet loads after this one and never redefines a token, so
@@ -34,6 +34,7 @@ import { RECIPE_CSS } from "../recipe-ui.js";
34
34
  import { REQUEST_LIMITS_CSS } from "../request-budget-ui.js";
35
35
  import { RETENTION_CSS } from "../retention-ui.js";
36
36
  import { crumbsHtml, SETTINGS_KIT_CSS } from "../settings-kit.js";
37
+ import { acts as kitActs, btn as kitBtn, doc as kitDoc, docHeader as kitDocHeader, notFound as kitNotFound, notice as kitNotice, outPage } from "./kit.js";
37
38
  import { SKILLS_CSS } from "../skills-ui.js";
38
39
  import { SPEND_CSS } from "../spend-ui.js";
39
40
  import { SSO_CSS } from "../sso-ui.js";
@@ -95,17 +96,23 @@ export function refuse(response, who, status, message, backHref = "/") {
95
96
  if (who === null || who.via === "bearer") {
96
97
  return respond(response, status, "text/plain; charset=utf-8", message);
97
98
  }
98
- const body = joinHtml([
99
- html `<h1>${status === 404 ? "Not found" : "Request refused"}</h1>`,
100
- // A missing page is plain news, not an error.
101
- status === 404 ? html `<p>${message}</p>` : html `<div class="problem">${message}</div>`,
102
- html `<p class="meta refusal-back"><a href="${backHref}">\u2190 Back</a></p>`,
103
- ], "\n");
99
+ // Kit pages (Wave E): a missing page is plain news, not an error, with two ways on; a refusal says what was refused
100
+ // where it happened and leads back.
101
+ const path = new URL(requestContext.getStore()?.returnTo ?? "/", "http://standing-orders.local").pathname;
102
+ const generic = /^there'?s no page at this address\.?$/i.test(message.replace("\u2019", "'"));
103
+ // The words are the caller's exactly (older callers wrote lower-case fragments; k-refusal capitalises the first letter).
104
+ const body = html `<div class="k-refusal">${status === 404
105
+ ? kitNotFound(path, generic ? {} : { title: message, text: "Home has what needs you; Tasks has every task." })
106
+ : kitDoc(kitDocHeader({ title: "Request refused", back: [backHref, "Back"] }), kitNotice(message), kitActs(kitBtn("Go back", { href: backHref })))}</div>`;
104
107
  // A signed-in browser keeps the workspace around it: the same navigation as every page.
105
108
  const framed = requestContext.getStore()?.refusal;
106
109
  if (framed !== undefined && who.via === "cookie")
107
110
  return framed(response, status, body);
108
- return page(response, status, shell("refused", body));
111
+ return page(response, status, shell(status === 404 ? "Not found" : "Request refused", outPage({
112
+ brand: BRAND_HTML, title: status === 404 ? "Not found" : "Request refused",
113
+ body: html `<div class="k-refusal">${kitNotice(status === 404 && generic ? `There\u2019s no page at ${path}.` : message)}</div>`,
114
+ foot: html `<a class="k-link" href="${status === 404 ? "/" : backHref}">${status === 404 ? "Go to Home" : "Go back"}</a>`,
115
+ })));
109
116
  }
110
117
  /** The demo's one-line promise, on every page. */
111
118
  export const DEMO_BANNER = `Demo: a scripted lead and sample projects. Nothing calls a model, reaches outside or spends. For your own project, run ${START_COMMAND} in its folder.`;
@@ -129,13 +136,16 @@ export const DEMO_BANNER_SHORT = "Demo: sample projects. Nothing calls a model o
129
136
  * React workspace, the pre-script fallback) reads these.
130
137
  *
131
138
  * The palette above, in light, dark and pinned themes, is Signal's generated
132
- * tokens.css (src/design, built by youi from the design-language repo); the
139
+ * tokens.css (src/design, generated by youiyoux from the toolroll System, src/design/system); the
133
140
  * console's own rules follow in console.css, shipped beside this module.
134
141
  * Their bytes are the served sheet's (WORKSPACE_STYLE hashes them). The React
135
142
  * workspace's stylesheet loads after this one and never redefines a token, so
136
143
  * a person's accent (accentHead) still wins over the generated default. */
137
144
  export const STYLE = readFileSync(join(import.meta.dirname, "..", "design", "tokens.css"), "utf8") +
138
- readFileSync(join(import.meta.dirname, "console.css"), "utf8");
145
+ readFileSync(join(import.meta.dirname, "console.css"), "utf8") +
146
+ // The revamp kit (src/design/KIT.md): k- classes only, after the console's rules, so the kit's markup from
147
+ // server pages (server/kit.ts) and React views (browser/kit/) looks the same everywhere.
148
+ readFileSync(join(import.meta.dirname, "..", "design", "kit.css"), "utf8");
139
149
  /** Appearance: a three-way segmented switch, one tap per choice. */
140
150
  export const THEME_CONTROLS_CSS = `.task-repo select{width:100%;min-height:2.75rem;font-size:1rem}.task-repo-add{margin:.35rem .1rem .5rem}.task-repo-add a{display:inline-flex;align-items:center;min-height:2.25rem}details.result-request-open.result-request-form>summary{border:0;background:transparent;padding:.5rem 0;min-height:2.75rem;font-weight:600;display:list-item;list-style:revert}details.result-request-open.result-request-form>summary::-webkit-details-marker{display:revert}form.js-autosave button[type=submit]{display:none}.provider-row{border-bottom:1px solid var(--so-line);padding:.35rem 0}.provider-row:first-of-type{border-top:1px solid var(--so-line)}.provider-head{display:flex;align-items:center;gap:.75rem;margin:.4rem 0 0}.provider-status{display:inline-flex;align-items:center;gap:.4rem;color:var(--so-muted);font-size:.875rem}.provider-status i{width:.5rem;height:.5rem;border-radius:50%;background:var(--so-muted)}.provider-status--ok i{background:var(--so-success)}.provider-status--warn i{background:var(--so-attention)}.provider-status--off i{background:transparent;border:1.5px solid var(--so-muted)}details.provider-manage>summary{cursor:pointer;color:var(--so-accent-text);font-size:.875rem;min-height:2.5rem;display:list-item;padding-block:.5rem}.card.props .row{display:grid;gap:.1rem;margin:0 0 .75rem}.card.props .row>.meta{display:block;font-size:.75rem}.card.props .row>.meta::first-letter{text-transform:uppercase}.card.props .row>.mono{font-family:var(--font-sans);font-size:.875rem}.card.props .row>.mono .seal{font-family:var(--font-mono);font-size:.8125rem}details.evidence-files{margin:1rem 0}details.evidence-files>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:600}details.evidence-files ul{list-style:none;margin:0;padding:0}details.evidence-files li{display:flex;justify-content:space-between;gap:1rem;padding:.5rem 0;border-bottom:1px solid var(--so-line)}.result-action .result-feedback-link{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;text-decoration:none}@media(hover:hover) and (pointer:fine){.result-action .result-feedback-link:hover{background:var(--so-raised)}}.so-sr-only{position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.verdict{margin:.5rem 0 .75rem}.verdict-chips{display:flex;flex-wrap:wrap;gap:.4rem;list-style:none;padding:0;margin:0}.verdict-chip{display:inline-flex;align-items:center;gap:.3rem;min-height:1.75rem;padding:.2rem .65rem;border-radius:999px;font-size:.8125rem;font-weight:600;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}.verdict-chip svg{width:.9rem;height:.9rem}.verdict-chip--success{background:var(--so-success-soft);color:var(--so-success)}.verdict-chip--danger{background:var(--so-danger-soft);color:var(--so-danger)}.verdict-chip--warning{background:var(--so-warning-soft);color:var(--so-warning)}.verdict-chip--info{background:var(--so-info-soft);color:var(--so-info)}.verdict-by{margin:.4rem 0 0}details.result-request-open{margin:.5rem 0}details.result-request-open>summary{display:inline-flex;align-items:center;min-height:2.5rem;padding:.5rem 1rem;border:1px solid var(--so-input-line);border-radius:.5rem;background:var(--so-paper);color:var(--so-ink);font-weight:600;cursor:pointer;list-style:none}details.result-request-open>summary::-webkit-details-marker{display:none}details.result-request-open[open]>summary{margin-bottom:.75rem}.settings-tiles{display:grid;gap:1.25rem;margin:0 0 2rem}.settings-tiles h2{margin:0 0 .5rem;font-size:.875rem;font-weight:600;color:var(--so-muted)}.settings-tiles section>div{display:grid;grid-template-columns:repeat(auto-fill,minmax(8.5rem,1fr));gap:.5rem}.settings-tiles a>span{display:grid;gap:.1rem;min-width:0}.settings-tiles .provider-status{font-weight:400;font-size:.8125rem}.settings-tiles a{display:flex;align-items:center;gap:.6rem;min-height:3rem;padding:.65rem .8rem;border:1px solid var(--so-line);border-radius:.625rem;background:var(--so-paper);color:var(--so-ink);text-decoration:none;font-weight:550;font-size:.875rem}@media(hover:hover) and (pointer:fine){.settings-tiles a:hover{border-color:var(--so-input-line);background:var(--so-raised)}}.settings-tiles a>svg{width:1.1rem;height:1.1rem;flex-shrink:0;color:var(--so-accent-text)}details.settings-more{margin:.25rem 0 1.25rem}details.settings-more>summary{cursor:pointer;min-height:2.75rem;display:list-item;padding-block:.7rem;font-weight:550}details.settings-more>summary .meta{font-weight:400;margin-left:.35rem}.settings-changed{margin-top:-.25rem}.appearance{margin:0 0 28px}.appearance h2{margin:0 0 10px}.theme-switch{display:inline-flex;flex-wrap:nowrap;max-width:100%;gap:4px;padding:4px;margin:0;border:1px solid var(--so-line);border-radius:10px;background:var(--so-raised)}.theme-switch .theme-choice,.so-native-region .theme-switch .theme-choice{flex:1 1 0;width:auto;white-space:nowrap;min-height:40px;padding:8px 16px;border:0;border-radius:7px;background:transparent;color:var(--so-muted);font:inherit;font-weight:550;box-shadow:none;cursor:pointer}@media(hover:hover) and (pointer:fine){.theme-switch .theme-choice:hover{color:var(--so-ink)}}.theme-switch .theme-choice[aria-pressed="true"]{background:var(--so-paper);color:var(--so-ink);box-shadow:0 1px 2px rgb(0 0 0 / .1)}.appearance .meta{margin:8px 0 0}@media(max-width:600px){.theme-switch .theme-choice{min-height:44px}}.update-notes{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit;max-height:18rem;overflow:auto}`;
141
151
  /** The page CSS this module writes itself (not the imported modules'), for the stylesheet contract tests. */
@@ -32,8 +32,8 @@
32
32
  }
33
33
  /* Signal's colour, shadow, radius, type and spacing tokens (--so-*) are
34
34
  * generated by youi into src/design/tokens.css, light and dark, and the
35
- * server prepends that sheet to this one (chrome.ts STYLE). Edit them in the
36
- * design-language repo, never here. The device decides the scheme unless the
35
+ * server prepends that sheet to this one (chrome.ts STYLE). Edit them in
36
+ * src/design/system/tokens and run youiyoux sync, never here. The device decides the scheme unless the
37
37
  * person pins one (html[data-theme], set from their cookie). What follows are
38
38
  * the console's older names: views onto those tokens, never a second ramp. */
39
39
  :root {
@@ -2742,3 +2742,32 @@ html.so-swap::view-transition-old(*):only-child { animation: so-card-out 160ms e
2742
2742
 
2743
2743
  .result-feedback-actions { display: flex; flex-wrap: wrap; gap: .5rem; }
2744
2744
  .result-feedback-actions button { min-height: 44px; white-space: nowrap; }
2745
+
2746
+ /* Put away old tasks (docs/plans/task-tidy.md): grouped checklist on the kit's column. */
2747
+ .put-away { max-width: var(--so-settings-width, 760px); }
2748
+ .put-away h1 { margin: 0 0 4px; }
2749
+ .put-away-days { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 16px; }
2750
+ .put-away-days label { display: flex; align-items: center; gap: 8px; margin: 0; font-size: 14px; }
2751
+ .put-away-days input[type=number] { width: 5rem; margin: 0; }
2752
+ .put-away-days button { width: auto; }
2753
+ .put-away-group { margin: 0 0 20px; padding: 0; border: 0; }
2754
+ .put-away-group legend { display: flex; align-items: center; gap: 8px; padding: 0; margin: 0 0 6px; font-weight: 600; font-size: 15px; }
2755
+ .put-away-group legend .count { font-family: var(--so-mono, ui-monospace, monospace); font-size: 11px; font-weight: 500; color: var(--so-muted); }
2756
+ .put-away-group ul { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--so-line); }
2757
+ .put-away-group li { border-bottom: 1px solid var(--so-line); }
2758
+ .put-away-item { display: grid; grid-template-columns: auto minmax(0, 1fr); column-gap: 10px; align-items: start; margin: 0; padding: 10px 2px; cursor: pointer; }
2759
+ .put-away-item input { margin: 3px 0 0; width: auto; min-height: 0; align-self: start; }
2760
+ .put-away-title { font-size: 13.5px; font-weight: 500; overflow-wrap: anywhere; }
2761
+ .put-away-why { grid-column: 2; font-size: 12.5px; color: var(--so-muted); }
2762
+ .put-away-id { grid-column: 2; font-family: var(--so-mono, ui-monospace, monospace); font-size: 11.5px; color: var(--so-muted); overflow-wrap: anywhere; }
2763
+ .put-away-cli { margin: 12px 0 0; font-size: 13px; color: var(--so-muted); overflow-wrap: anywhere; }
2764
+ .put-away-cli code { font-size: 12px; }
2765
+ .task-archived { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; margin: 0 0 12px; padding: 10px 12px; border-radius: 8px; background: var(--so-raised); }
2766
+ .task-archived p { margin: 0; font-size: 13.5px; }
2767
+ .task-archived form { margin: 0; }
2768
+ .task-archived button { width: auto; }
2769
+ .work-row-status form.inline { margin: 0; }
2770
+ @media (max-width: 900px) {
2771
+ .put-away-item { min-height: 44px; }
2772
+ .put-away-days button, .task-archived button, .work-row-status form.inline button { min-height: 44px; }
2773
+ }
@@ -11,7 +11,9 @@ import { LIMITS } from "../decision.js";
11
11
  import { run as execRun } from "../exec.js";
12
12
  import { setFlowSecret } from "../flow-actions.js";
13
13
  import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decideFlowCard, flowDefinitionOf, moveCardInFlow } from "../flow-engine.js";
14
- import { galleryHtml, galleryUseHtml } from "../flow-gallery-ui.js";
14
+ import { galleryUseHtml, newFlowHtml } from "../flow-gallery-ui.js";
15
+ import { startersHtml } from "../flow-starters-ui.js";
16
+ import { starterOf, startersFor } from "../flow-starters.js";
15
17
  import { galleryAdmitsAnyone, galleryDefaults, galleryDiagram, galleryTemplateOf, galleryToolsOf, previewGallery, useGalleryTemplate } from "../flow-gallery.js";
16
18
  import { flowInsights } from "../flow-insights.js";
17
19
  import { assignFlowCard, commentOnFlowCard, watchFlowCard } from "../flow-people.js";
@@ -21,13 +23,14 @@ import { exportFlow, fetchFlowFile, FlowFileError, importFlow, parseFlowFile, pl
21
23
  import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, setFlowTriggerOn, renewFlowHook, saveHooksBase, saveLinearKey, saveSigningSecret, setUpFlowHook, changeFlowHookSender, setFlowHookScope, shareFlowButton, stopSharingFlowButton, rawTakesDeliveries, rawAdmitsAnyone } from "../flow-triggers.js";
22
24
  import { FLOW_IMPORT_SCRIPT, flowFallbackHtml, flowImportHtml, flowsListHtml, flowView } from "../flows-ui.js";
23
25
  import { FLOW_TEMPLATES, FlowContractError, validateFlowDefinition, withZoneNames } from "../flows.js";
26
+ import { revokeFlowBrowser, signFlowBrowser } from "../flow-browser.js";
24
27
  import { prepareUnattendedCandidates, parseUnattendedAsk, revokeUnattended, signUnattended, untilWords } from "../flow-unattended.js";
25
28
  import { keyStatus } from "../keys.js";
26
- import { kitPageHtml, kitsGalleryHtml } from "../kits-ui.js";
29
+ import { kitPageHtml, kitRowsHtml } from "../kits-ui.js";
27
30
  import { addKitGithubTrigger, addKitSample, kitOf, setUpKit } from "../kits.js";
28
31
  import { connectionsOf } from "../mcp-connect.js";
29
32
  import { authorizedProject, projectName, } from "../project.js";
30
- import { recipeAnswersFromForm, recipeDefinitionPreviewHtml, recipeEditorHtml, recipeFromForm, recipeLibraryHtml, recipeRunHtml, recipeScript, workflowPreviewHtml } from "../recipe-ui.js";
33
+ import { recipeAnswersFromForm, recipeDefinitionPreviewHtml, recipeEditorHtml, recipeFromForm, recipeRowsHtml, recipeRunHtml, recipeScript, workflowPreviewHtml } from "../recipe-ui.js";
31
34
  import { createWorkflowPreview, exportRecipe, findRecipe, importRecipe, launchWorkflow, prepareRecipeRun, RecipeError, savedRecipes, saveWorkflowRecipe, starterRecipes, workflowPreview } from "../recipes.js";
32
35
  import { isAlive as runnerAlive } from "../runner.js";
33
36
  import { createSubagentFrom, labelOf, nameOf, saveSoul, sendSubagentSummaries, setSubagentState, subagentSettings } from "../subagent-admin.js";
@@ -58,7 +61,7 @@ export function createFlowsHandlers(runtime) {
58
61
  const diagram = preview?.built.definition ?? galleryDiagram(template);
59
62
  const page = galleryUseHtml({ template, projects: projects.map(path => ({ path, name: projectName(path) })), repo, answers, name, preview, problem,
60
63
  csrf: who.via === "cookie" ? who.session.csrf : "", diagram, tools: preview?.tools ?? galleryToolsOf(template, diagram, connectionsOf(store, repo)), said });
61
- return sendScreen(response, problem !== null && preview === null && notice !== null ? 400 : 200, screen(template.name, html `<p><a href="/flows/new">New flow</a></p><h1>${template.name}</h1>${page}`, { chrome: chromeFor(repo, "flows") }));
64
+ return sendScreen(response, problem !== null && preview === null && notice !== null ? 400 : 200, screen(template.name, page, { chrome: chromeFor(repo, "flows") }));
62
65
  }
63
66
  // ---- path parameters and shared lookups ------------------------------------
64
67
  /** One segment of the path the route's pattern admitted ("/flows/1/cards/2/move" → 2 is "1", 4 is "2"). */
@@ -75,15 +78,50 @@ export function createFlowsHandlers(runtime) {
75
78
  };
76
79
  const sendJson = (response, status, payload) => respond(response, status, "application/json; charset=utf-8", JSON.stringify(payload));
77
80
  // ---- flows: reads ----------------------------------------------------------
78
- // Flows → New: the gallery, and each template's page.
81
+ // Flows → New: describe it to the lead, or start from a template (with the kits, recipes and starter flows as groups).
79
82
  async function newFlowGallery(ctx) {
80
83
  const { url, response, who, project } = ctx;
81
84
  const projects = consoleProjects();
82
- const canUse = who.via === "cookie" && who.role === "approver" && projects.length > 0;
83
- // The chosen project, else the one being looked at: its tools are what each card says is connected.
85
+ const approver = who.via === "cookie" && who.role === "approver";
86
+ const canUse = approver && projects.length > 0;
87
+ // The chosen project, else the one being looked at, else the first: its tools are what each row says is connected.
84
88
  const asked = url.searchParams.get("repo");
85
- const repo = asked !== null && projects.includes(asked) ? asked : project !== null && projects.includes(project) ? project : null;
86
- return sendScreen(response, 200, screen("New flow", html `<p><a href="/flows">Flows</a></p><h1>New flow</h1>${(galleryHtml({ repo, canUse, connections: repo === null ? null : connectionsOf(store, repo) }))}`, { chrome: chromeFor(repo ?? project, "flows") }));
89
+ const repo = asked !== null && projects.includes(asked) ? asked : project !== null && projects.includes(project) ? project : projects[0] ?? null;
90
+ // Describe what you want: the words go to the lead as a chat draft, which drafts the flow for the person to confirm.
91
+ const described = url.searchParams.get("describe");
92
+ let askError = null;
93
+ if (described !== null && canUse) {
94
+ const words = described.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, "").trim();
95
+ const named = projects.find(one => projectName(one) === url.searchParams.get("project")) ?? repo;
96
+ if (words !== "")
97
+ return redirect(response, `/chat?draft=${encodeURIComponent(`Make a flow${named === null ? "" : ` in ${projectName(named)}`}: ${words}`.slice(0, 4000))}`);
98
+ askError = "Describe what should happen first.";
99
+ }
100
+ // One-time work: the open project's recipes (recipe screens work in the open project).
101
+ let recipes;
102
+ try {
103
+ const revision = who.via === "cookie" ? who.session.projectRevision : 0;
104
+ const recent = project === null ? [] : store.handle.prepare("SELECT document,task_id,flow_id FROM workflow_preview WHERE repo=? AND (task_id IS NOT NULL OR flow_id IS NOT NULL) ORDER BY created_at DESC LIMIT 12").all(project).map(row => {
105
+ const d = importRecipe(String(row["document"]));
106
+ const task = row["task_id"] === null ? null : store.getTask(String(row["task_id"]));
107
+ const flow = row["flow_id"] === null ? null : store.getFlow(Number(row["flow_id"]));
108
+ const on = flow !== null && flow.state === "active" && store.flowTriggers(flow.id).some(one => one.kind === "schedule" && one.state === "active");
109
+ return { name: d.name, href: task !== null ? taskHref(task.id) : `/flows/${Number(row["flow_id"])}`, state: task?.state ?? (flow === null || flow.state !== "active" ? "Archived" : on ? "Scheduled" : "Paused") };
110
+ });
111
+ recipes = recipeRowsHtml(starterRecipes(), project === null ? [] : savedRecipes(store, who.name, project), project, project === null ? null : projectName(project), revision, recent);
112
+ }
113
+ catch (error) {
114
+ if (!(error instanceof RecipeError))
115
+ throw error;
116
+ recipes = html `<p class="k-notice" role="alert">${error.message}</p>`;
117
+ }
118
+ const starters = repo === null ? html `` : startersHtml({ repo, projectName: projectName(repo), starters: startersFor(store, repo), canSwitch: approver && !store.isDemo(),
119
+ suggested: starterOf(url.searchParams.get("starter") ?? "")?.id ?? null });
120
+ const said = url.searchParams.get("said"), problem = url.searchParams.get("problem");
121
+ const page = newFlowHtml({ repo, projects: projects.map(path => ({ path, name: projectName(path) })), canUse, group: url.searchParams.get("group") ?? "all",
122
+ connections: repo === null ? null : connectionsOf(store, repo), ask: { value: described ?? "", error: askError }, kits: kitRowsHtml(store, repo), recipes, starters,
123
+ said: said === null ? null : said.slice(0, 400), problem: problem === null ? null : problem.slice(0, 400) });
124
+ return sendScreen(response, 200, screen("New flow", page, { chrome: chromeFor(repo ?? project, "flows"), functional: { script: FLOW_IMPORT_SCRIPT } }));
87
125
  }
88
126
  async function galleryTemplatePage(ctx) {
89
127
  const { url, who, response, project } = ctx;
@@ -106,7 +144,7 @@ export function createFlowsHandlers(runtime) {
106
144
  const { url, who, response, project } = ctx;
107
145
  const projects = consoleProjects();
108
146
  const flows = store.listFlows(projects);
109
- return sendScreen(response, 200, screen("Flows", html `<h1>Flows</h1>${(flowsListHtml(store, flows, projects, who.via === "cookie" && who.role === "approver", url.searchParams.get("problem")))}`, { chrome: chromeFor(project, "flows"), functional: { script: FLOW_IMPORT_SCRIPT } }));
147
+ return sendScreen(response, 200, screen("Flows", flowsListHtml(store, flows, projects, who.via === "cookie" && who.role === "approver", url.searchParams.get("problem"), url.searchParams.get("project"), clock()), { chrome: chromeFor(project, "flows"), functional: { script: FLOW_IMPORT_SCRIPT } }));
110
148
  }
111
149
  async function flowInsightsRead(ctx) {
112
150
  const { url, response } = ctx;
@@ -156,10 +194,10 @@ export function createFlowsHandlers(runtime) {
156
194
  }
157
195
  // ---- starter kits: the gallery, and each kit's checklist in a project ------
158
196
  async function kitsGallery(ctx) {
159
- const { url, who, response, project } = ctx;
160
- const projects = consoleProjects();
161
- const canSetUp = who.via === "cookie" && who.role === "approver";
162
- return sendScreen(response, 200, screen("Starter kits", html `<h1>Starter kits</h1>${(kitsGalleryHtml(store, projects, projectName, canSetUp, { problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(project, "flows") }));
197
+ const { url, response } = ctx;
198
+ // Starter kits live under New flow → With an agent now; a refusal sent here still shows there.
199
+ const problem = url.searchParams.get("problem");
200
+ return redirect(response, `/flows/new?group=kits${problem === null ? "" : `&problem=${encodeURIComponent(problem.slice(0, 400))}`}`);
163
201
  }
164
202
  async function kitPage(ctx) {
165
203
  const { url, who, response } = ctx;
@@ -168,8 +206,8 @@ export function createFlowsHandlers(runtime) {
168
206
  const kit = kitOf(pathPart(url, 2));
169
207
  const repo = url.searchParams.get("repo") ?? (projects.length === 1 ? projects[0] : "");
170
208
  if (kit === null || !projects.includes(repo))
171
- return redirect(response, "/kits");
172
- return sendScreen(response, 200, screen(kit.name, html `<h1>${kit.name}</h1>${(kitPageHtml(store, kit, repo, options.configDir ?? null, canSetUp, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(repo, "flows") }));
209
+ return redirect(response, "/flows/new?group=kits");
210
+ return sendScreen(response, 200, screen(kit.name, kitPageHtml(store, kit, repo, options.configDir ?? null, canSetUp, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }, projectName), { chrome: chromeFor(repo, "flows") }));
173
211
  }
174
212
  // ---- v92 / D5: the lead's subagents — the list, and one page per subagent ----
175
213
  async function subagentsPage(ctx) {
@@ -199,24 +237,9 @@ export function createFlowsHandlers(runtime) {
199
237
  response.writeHead(200, { ...SAFETY, "Content-Type": "text/markdown; charset=utf-8", "Content-Disposition": `attachment; filename="${mate.handle}.md"` });
200
238
  return void response.end(mate.soul);
201
239
  }
240
+ /** Recipes are New flow → One-time work now: the old address lands there. */
202
241
  async function recipeLibrary(ctx) {
203
- const { who, response, project } = ctx;
204
- try {
205
- const revision = who.via === "cookie" ? who.session.projectRevision : 0;
206
- const recent = project === null ? [] : store.handle.prepare("SELECT document,task_id,flow_id FROM workflow_preview WHERE repo=? AND (task_id IS NOT NULL OR flow_id IS NOT NULL) ORDER BY created_at DESC LIMIT 12").all(project).map(row => {
207
- const d = importRecipe(String(row["document"]));
208
- const task = row["task_id"] === null ? null : store.getTask(String(row["task_id"]));
209
- const flow = row["flow_id"] === null ? null : store.getFlow(Number(row["flow_id"]));
210
- const on = flow !== null && flow.state === "active" && store.flowTriggers(flow.id).some(one => one.kind === "schedule" && one.state === "active");
211
- return { name: d.name, href: task !== null ? taskHref(task.id) : `/flows/${Number(row["flow_id"])}`, state: task?.state ?? (flow === null || flow.state !== "active" ? "Archived" : on ? "Scheduled" : "Paused") };
212
- });
213
- return sendScreen(response, 200, screen("Workflow recipes", recipeLibraryHtml(starterRecipes(), project === null ? [] : savedRecipes(store, who.name, project), project, revision, recent), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
214
- }
215
- catch (error) {
216
- if (error instanceof RecipeError)
217
- return refuse(response, who, error.status, error.message, "/recipes");
218
- throw error;
219
- }
242
+ return redirect(ctx.response, "/flows/new?group=recipes");
220
243
  }
221
244
  /** One recipe screen in the open project: the editor (start, new, edit, from a task), use, preview, export; any other address is no screen. */
222
245
  const recipeScreen = (shown) => async (ctx) => {
@@ -536,7 +559,7 @@ export function createFlowsHandlers(runtime) {
536
559
  // What was previewed: the file and every value it resolved to (a default the first preview filled in included).
537
560
  const previewedOf = (plan) => createHash("sha256").update(`${document}\0${JSON.stringify(plan?.values ?? given)}`).digest("hex").slice(0, 32);
538
561
  const csrf = who.via === "cookie" ? who.session.csrf : "";
539
- const page = (plan, problem, status) => sendScreen(response, status, screen(`Import ${file.name}`, html `<p><a href="/flows">Flows</a></p><h1>Import ${file.name}</h1>${(flowImportHtml({ plan, file, document, repo, csrf, values: given, previewed: previewedOf(plan), problem }))}`, { chrome: chromeFor(repo, "flows") }));
562
+ const page = (plan, problem, status) => sendScreen(response, status, screen(`Import ${file.name}`, flowImportHtml({ plan, file, document, repo, csrf, values: given, previewed: previewedOf(plan), problem }), { chrome: chromeFor(repo, "flows") }));
540
563
  let plan;
541
564
  try {
542
565
  plan = planFlowImport(store, repo, file, given, who.name);
@@ -613,8 +636,14 @@ export function createFlowsHandlers(runtime) {
613
636
  }
614
637
  if (action === "scripts") {
615
638
  // The project's script library, reached from any of its flows.
616
- if (body.get("approve") === "yes")
617
- return store.approveFlowScript(flow.repo, body.get("name") ?? "") ? settle("Approved. Zones that run it go on.") : answer(404, { ok: false, said: "That script isn't waiting for approval." });
639
+ if (body.get("approve") === "yes") {
640
+ // The approval covers the version and content the panel showed (its version and digest ride back).
641
+ const version = Number(body.get("version")), digest = body.get("digest") ?? "";
642
+ const approved = Number.isSafeInteger(version) && version > 0 && digest !== "" ? store.approveFlowScript(flow.repo, body.get("name") ?? "", { version, digest }) : "changed";
643
+ return approved === "approved" ? settle("Approved. Zones that run it go on.")
644
+ : approved === "changed" ? answer(409, { ok: false, said: "This script changed since you read it. Read the new script, then approve it. Nothing was approved." })
645
+ : answer(404, { ok: false, said: "That script isn't waiting for approval." });
646
+ }
618
647
  if (body.get("remove") === "yes")
619
648
  return store.removeFlowScript(flow.repo, body.get("name") ?? "", who.name, now) ? settle("Script removed. Zones that ran it wait until it's back.") : answer(404, { ok: false, said: "There's no script by that name." });
620
649
  const saved = saveScript(store, flow.repo, { name: body.get("name"), about: body.get("about"), body: body.get("body"), timeoutMinutes: body.get("timeoutMinutes"), language: body.get("language"), file: body.get("file") }, who.name, now);
@@ -656,6 +685,18 @@ export function createFlowsHandlers(runtime) {
656
685
  if (action === "unattended-off") {
657
686
  return revokeUnattended(store, flow.id, who.name, now) ? settle("It no longer runs on its own. New cards wait for you.") : answer(409, { ok: false, said: "It wasn't running on its own." });
658
687
  }
688
+ if (action === "browser") {
689
+ // A person's own step-up (a lead's or remote run's token never gets here: route table), then signFlowBrowser
690
+ // re-reads the flow, its zones and scripts in its one write and refuses unless they are the digest the panel showed.
691
+ const proved = authenticateApprover(who, body.get("password") ?? "", flow.repo);
692
+ if (!proved.ok)
693
+ return answer(403, { ok: false, said: "That password isn't right. Nothing was approved." });
694
+ const signed = signFlowBrowser(store, { flow: flow.id, signer: who.name, proof: proved.method, sawDigest: body.get("digest") ?? "", console: { generation: who.via === "cookie" ? who.session.generation : -1 } }, now);
695
+ return signed.ok ? settle(`Browser allowed. ${signed.grant.zones.length === 1 ? "Its script" : "Its scripts"} can start Chromium now.`) : answer(409, { ok: false, said: signed.message });
696
+ }
697
+ if (action === "browser-off") {
698
+ return revokeFlowBrowser(store, flow.id, who.name, now) ? settle("Browser turned off. Its browser zones wait until someone allows it again.") : answer(409, { ok: false, said: "Browser use wasn't allowed for this flow." });
699
+ }
659
700
  if (action === "save") {
660
701
  let saved, drawn;
661
702
  try {
@@ -975,6 +1016,8 @@ export function createFlowsHandlers(runtime) {
975
1016
  "flow.secrets": flowAct("secrets"),
976
1017
  "flow.unattended": flowAct("unattended"),
977
1018
  "flow.unattended-off": flowAct("unattended-off"),
1019
+ "flow.browser": flowAct("browser"),
1020
+ "flow.browser-off": flowAct("browser-off"),
978
1021
  "flow.triggers": flowAct("triggers"),
979
1022
  "flow.card.move": cardAct("move"),
980
1023
  "flow.card.decide": cardAct("decide"),
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Home ("/", UI revamp wave B): one sentence that answers "what needs me?", four counts read from the same work index
3
+ * the Tasks list filters (so a count and the list it opens always agree), what waits on this person grouped by what it
4
+ * asks, what is running, and the lead's message box. Reads only: every act keeps its own route and rules (an agent's
5
+ * question is answered through POST /d/<id>/answer, the same one-click act as its own page).
6
+ */
7
+ import type { BrowserHomeView } from "../browser-workspace.js";
8
+ import type { Store } from "../store.js";
9
+ import type { WorkSummaryAccess } from "../work-summary.js";
10
+ /** How many waiting rows Home lists; the rest open in Tasks. */
11
+ export declare const HOME_NEEDS_SHOWN = 30;
12
+ export type HomeFacts = {
13
+ store: Store;
14
+ now: Date;
15
+ access: WorkSummaryAccess;
16
+ evidenceRoot?: string;
17
+ /** The project in view (already admitted), or null for every project. */
18
+ project: string | null;
19
+ /** Every admitted project, for the scope links. */
20
+ projects: readonly {
21
+ name: string;
22
+ path: string;
23
+ }[];
24
+ /** Chat is allowed for this person here (the lead's message box). */
25
+ chat: boolean;
26
+ /** This person can answer an agent's question (an approver in a browser session). */
27
+ canAnswer: boolean;
28
+ };
29
+ /** "7 need you across 2 projects." — the sentence and the figures read the same totals. */
30
+ export declare function homeSentence(needs: number, perProject: readonly {
31
+ name: string;
32
+ needs: number;
33
+ }[], scope: string | null, any: boolean): string;
34
+ /** Home's view for one person and scope, from two reads of the work index (what waits, what runs). */
35
+ export declare function homeView(facts: HomeFacts): BrowserHomeView;
@@ -0,0 +1,104 @@
1
+ import { browserWorkActionHref } from "../browser-crew.js";
2
+ import { ASK_LABEL } from "../needs-you.js";
3
+ import { projectName } from "../project.js";
4
+ import { workIndexPage } from "../work-index.js";
5
+ import { relativeAge } from "./chrome.js";
6
+ import { taskHref } from "./http.js";
7
+ const TONE = { attention: "needs", ready: "needs", live: "live", done: "done", problem: "failed", muted: "", neutral: "" };
8
+ const ASKS = ["decide", "review", "unblock"];
9
+ const ASK_WORD = { decide: "Decision", review: "Result", unblock: "Blocked" };
10
+ /** How many waiting rows Home lists; the rest open in Tasks. */
11
+ export const HOME_NEEDS_SHOWN = 30;
12
+ const HOME_RUNNING_SHOWN = 8;
13
+ /** "7 need you across 2 projects." — the sentence and the figures read the same totals. */
14
+ export function homeSentence(needs, perProject, scope, any) {
15
+ if (!any)
16
+ return "No tasks yet.";
17
+ if (needs === 0)
18
+ return scope === null ? "Nothing needs you." : `Nothing needs you in ${scope}.`;
19
+ const verb = needs === 1 ? "needs" : "need";
20
+ if (scope !== null)
21
+ return `${needs} ${verb} you in ${scope}.`;
22
+ const waiting = perProject.filter(one => one.needs > 0);
23
+ return waiting.length === 1 ? `${needs} ${verb} you in ${waiting[0].name}.` : `${needs} ${verb} you across ${waiting.length} projects.`;
24
+ }
25
+ function rowOf(store, item, now, multi, canAnswer) {
26
+ const action = item.primaryAction;
27
+ const href = action?.code === "open-result" && action.target.runId != null
28
+ ? `/review?result=${encodeURIComponent(action.target.taskId)}&run=${action.target.runId}` : browserWorkActionHref(item);
29
+ const decisionId = action?.target.decisionId ?? null;
30
+ const decision = decisionId === null ? null : store.getDecision(decisionId);
31
+ const open = decision !== null && decision.state === "open" && decision.supersededReason === null;
32
+ // An irreversible option is armed behind a confirmation on its own page; Home answers only what one click may.
33
+ const question = open && canAnswer && decision.options.every(option => option.reversible) ? {
34
+ id: decision.id, text: decision.question, recap: decision.recap,
35
+ options: decision.options.map(option => ({ id: option.id, label: option.label, consequence: option.consequence, recommended: option.id === decision.recommendation })),
36
+ } : null;
37
+ const waits = item.status.views.includes("needs-you");
38
+ // The specific ask, never the group's own words again: "Needs you" under Unblock reads "Blocked".
39
+ const word = waits ? (open ? "Question" : item.chip ?? (item.status.label === "Needs you" && item.ask !== null ? ASK_WORD[item.ask] : item.status.label)) : item.status.label;
40
+ const tone = waits ? "ask" : TONE[item.status.tone];
41
+ const line = waits
42
+ ? (open ? decision.question : item.status.detail !== item.status.label ? item.status.detail : null)
43
+ : item.progress ?? (item.status.detail !== item.status.label ? item.status.detail : null);
44
+ return {
45
+ id: item.rootId, title: item.title, href: taskHref(item.rootId),
46
+ project: multi || item.repo === null ? (item.repo === null ? "Unplaced" : projectName(item.repo)) : null,
47
+ age: relativeAge(item.updatedAt, now), chip: { word, tone }, line,
48
+ action: href === null || action === null ? null : { label: action.label, href }, question,
49
+ };
50
+ }
51
+ /** Home's view for one person and scope, from two reads of the work index (what waits, what runs). */
52
+ export function homeView(facts) {
53
+ const { store, now, access, project } = facts;
54
+ const read = (view, limit) => workIndexPage(store, now, access, { view, limit, project, ...(facts.evidenceRoot === undefined ? {} : { root: facts.evidenceRoot }) });
55
+ const needs = read("needs-you", HOME_NEEDS_SHOWN);
56
+ const running = read("running", HOME_RUNNING_SHOWN);
57
+ const totals = needs.totals;
58
+ // Queued: the Tasks list's own Queued count (the same predicate its view=queued filter lists).
59
+ const queued = totals.queued;
60
+ // Every project is said by name (all=1): /work alone would narrow to the session's open project.
61
+ const scopeQuery = project === null ? "all=1&" : `project=${encodeURIComponent(project)}&`;
62
+ const counts = [
63
+ { key: "needs", label: "Needs you", value: totals["needs-you"], href: `/work?${scopeQuery}view=needs-you` },
64
+ { key: "running", label: "Running", value: totals.running, href: `/work?${scopeQuery}view=running` },
65
+ { key: "queued", label: "Queued", value: queued, href: `/work?${scopeQuery}view=queued` },
66
+ { key: "finished", label: "Finished", value: totals.completed, href: `/work?${scopeQuery}view=completed` },
67
+ ];
68
+ const multi = project === null && new Set([...needs.items, ...running.items].map(one => one.repo ?? "")).size > 1;
69
+ const groups = [];
70
+ for (const ask of ASKS) {
71
+ const rows = needs.items.filter(one => one.ask === ask).map(one => rowOf(store, one, now, multi, facts.canAnswer));
72
+ if (rows.length > 0)
73
+ groups.push({ key: ask, label: ASK_LABEL[ask], rows });
74
+ }
75
+ // A waiting row whose ask the index couldn't name still waits on this person: it goes under Decide, never lost.
76
+ const unnamed = needs.items.filter(one => one.ask === null).map(one => rowOf(store, one, now, multi, facts.canAnswer));
77
+ if (unnamed.length > 0) {
78
+ const decide = groups.find(one => one.key === "decide");
79
+ if (decide)
80
+ decide.rows.push(...unnamed);
81
+ else
82
+ groups.unshift({ key: "decide", label: ASK_LABEL.decide, rows: unnamed });
83
+ }
84
+ const live = running.items.filter(one => !one.status.views.includes("needs-you")).map(one => rowOf(store, one, now, multi, facts.canAnswer));
85
+ if (live.length > 0)
86
+ groups.push({ key: "running", label: "Running", rows: live });
87
+ const perProject = needs.projects.map(one => ({ name: one.repo === null ? "Unplaced" : projectName(one.repo), needs: one.totals["needs-you"] }));
88
+ const any = totals.all > 0 || totals.completed > 0 || (totals.archived ?? 0) > 0;
89
+ const shown = groups.filter(one => one.key !== "running").reduce((sum, one) => sum + one.rows.length, 0);
90
+ return {
91
+ kind: "home",
92
+ say: homeSentence(totals["needs-you"], perProject, project === null ? null : projectName(project), any),
93
+ project,
94
+ scopes: facts.projects.length < 2 ? null : [
95
+ { key: "", label: "All projects", href: "/", active: project === null },
96
+ ...facts.projects.map(one => ({ key: one.path, label: one.name, href: `/?project=${encodeURIComponent(one.path)}`, active: one.path === project })),
97
+ ],
98
+ counts, groups,
99
+ more: totals["needs-you"] > shown ? { count: totals["needs-you"] - shown, href: counts[0].href } : null,
100
+ newTask: "/tasks/new",
101
+ ask: facts.chat ? { project, placeholder: project === null ? "Ask the lead about your projects…" : `Ask the lead about ${projectName(project)}…` } : null,
102
+ empty: any ? null : { title: "No tasks yet.", text: "Describe a change in a sentence; an agent writes a plan for you to approve." },
103
+ };
104
+ }