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
@@ -1,79 +1,89 @@
1
1
  /**
2
2
  * Settings → Updates: the installed version, one of three ways to update
3
3
  * (behind the operator's password), the update's steps live while it runs,
4
- * and a one-time "What's new" card afterwards. The work itself is the
4
+ * and a one-time "What's new" section afterwards. The work itself is the
5
5
  * `toolroll update` command (toolroll-update.ts), started as its own job.
6
+ * Built from the revamp kit; the Settings frame adds the title.
6
7
  */
7
- import { html, postForm, replaceMarkup, reauth } from "./html.js";
8
+ import { html, joinHtml, postForm } from "./html.js";
8
9
  import { STEP_WORDS, UPDATE_STEPS, runtimeUpdateTerminal } from "./toolroll-update.js";
10
+ import { btn, chip, codeLine, disclose, formRow, infoLine, note, notice, radios, settingsRecord, settingsSection } from "./server/kit.js";
11
+ import { kitSectionForm } from "./settings-kit.js";
9
12
  const clock = (iso) => { const d = new Date(iso); return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`; };
10
- export const UPDATES_CSS = `.updates{max-width:640px;min-width:0;overflow-wrap:anywhere}.updates .card{margin:0 0 16px}.updates h2{margin:0;font-size:1.0625rem}.updates .card>p{margin:4px 0 0}` +
11
- `.updates .update-actions{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0 0}.updates .update-actions button{white-space:nowrap}` +
12
- `.updates button.primary{background:var(--primary);color:var(--primary-foreground);border-color:var(--primary);font-weight:600}` +
13
- `.updates .step-up{display:grid;gap:4px;font-size:.8125rem;margin:16px 0 0;max-width:20rem}.updates .step-up input{min-height:40px}` +
14
- `.updates ol.update-steps{list-style:none;padding:0;margin:14px 0 0;display:grid;gap:2px}.updates ol.update-steps li{display:grid;grid-template-columns:18px minmax(0,1fr);gap:0 10px;align-items:baseline;padding:5px 0;font-size:.875rem;color:var(--so-muted)}` +
15
- `.updates ol.update-steps li::before{content:"";width:8px;height:8px;border-radius:50%;border:1.5px solid var(--so-input-line);justify-self:center;transform:translateY(-1px)}` +
16
- `.updates ol.update-steps li[data-state=done]{color:var(--so-ink)}.updates ol.update-steps li[data-state=done]::before{background:var(--so-success);border-color:var(--so-success)}` +
17
- `.updates ol.update-steps li[data-state=now]{color:var(--so-ink);font-weight:600}.updates ol.update-steps li[data-state=now]::before{background:var(--so-info);border-color:var(--so-info);box-shadow:0 0 0 4px color-mix(in srgb,var(--so-info) 16%,transparent)}` +
18
- `.updates ol.update-steps li[data-state=failed]{color:var(--so-danger);font-weight:600}.updates ol.update-steps li[data-state=failed]::before{background:var(--so-danger);border-color:var(--so-danger)}` +
19
- `.updates ol.update-steps .step-detail{grid-column:2;font-weight:400;color:var(--so-muted);font-size:.8125rem;margin-top:2px}` +
20
- `.updates .whats-new ul{margin:10px 0 0;padding-left:18px}.updates .whats-new li{margin:4px 0}.updates .whats-new form{margin:14px 0 0}` +
21
- `.updates .update-problem{color:var(--so-danger)}.updates .stamp{margin:10px 0 0}.updates code{white-space:nowrap}.updates [data-update-outcome=stopped] code,.updates .step-detail code{white-space:normal;overflow-wrap:anywhere}` +
22
- `@media (prefers-reduced-motion:no-preference){.updates ol.update-steps li[data-state=now]::before{animation:update-pulse 1.6s ease-in-out infinite}}@keyframes update-pulse{50%{box-shadow:0 0 0 7px color-mix(in srgb,var(--so-info) 6%,transparent)}}` +
23
- `@media (max-width:600px){.updates .update-actions{display:grid}.updates .update-actions button{width:100%;min-height:44px}.updates .step-up{max-width:none}.updates .step-up input{min-height:44px}}`;
13
+ /** The page is kit pieces now; nothing of the old look is left. Kept as an export for the shared stylesheet. */
14
+ export const UPDATES_CSS = "";
15
+ /** A step's state as one of the kit's status words. */
16
+ const STEP_CHIP = {
17
+ done: chip("Finished", "done"), now: chip("Running", "live"), failed: chip("Failed", "failed"), waiting: chip("Queued", ""),
18
+ };
24
19
  /** The steps, each done, current, failed or waiting. Also the live region's fragment. */
25
- export function updateStepsHtml(j, running) {
20
+ export function updateStepsHtml(j, running, options = {}) {
26
21
  const finished = runtimeUpdateTerminal(j.phase);
27
22
  const failed = ["restored", "refused", "needs-attention", "rolling-back"].includes(j.phase);
28
23
  const last = UPDATE_STEPS.indexOf([...j.steps].reverse().find(s => UPDATE_STEPS.includes(s.phase))?.phase);
29
24
  const items = UPDATE_STEPS.map((step, i) => {
30
25
  const state = j.phase === "complete" || i < last ? "done" : i > last ? "waiting" : failed ? "failed" : finished ? "waiting" : "now";
31
26
  // Only where it adds something: which work it waits for, or what went wrong.
32
- const detail = state === "now" && step === "draining" && j.waiting ? html `<span class="step-detail">${j.waiting.on}.${j.waiting.action ? html ` If nothing of it is running, run <code>${j.waiting.action}</code>.` : ""}</span>`
33
- : (state === "now" && step === "draining") || (state === "failed" && j.phase === "rolling-back") ? html `<span class="step-detail">${j.detail}</span>` : "";
34
- return html `<li data-step="${step}" data-state="${state}">${STEP_WORDS[step]}${detail}</li>`;
27
+ const detail = state === "now" && step === "draining" && j.waiting ? html `${j.waiting.on}.${j.waiting.action ? html ` If nothing of it is running, run <code class="k-mono">${j.waiting.action}</code>.` : ""}`
28
+ : (state === "now" && step === "draining") || (state === "failed" && j.phase === "rolling-back") ? html `${j.detail}` : "";
29
+ return settingsRecord({ title: STEP_WORDS[step], sub: detail, when: STEP_CHIP[state], attrs: { "data-step": step, "data-state": state } });
35
30
  });
36
31
  const title = j.phase === "scheduled" ? `Update to ${j.to.version} scheduled for ${clock(j.at ?? j.startedAt)}`
37
32
  : j.phase === "rolling-back" ? `Restoring ${j.from.version}`
38
33
  : j.kind === "rollback" ? `Rolling back to ${j.to.version}` : `Updating to ${j.to.version}`;
39
- const stalled = !running && !finished ? html `<p class="update-problem" role="alert">The updater stopped. Run <code>toolroll update --resume</code> to continue it.</p>` : "";
40
- return html `<div id="update-live" data-phase="${j.phase}" data-done="${finished ? 1 : 0}"><h2>${title}</h2>${j.phase === "scheduled" ? html `<p class="meta">Running work finishes first. You can close this page.</p>` : html `<ol class="update-steps" aria-label="Update steps">${items}</ol>`}${stalled}</div>`;
34
+ const stalled = !running && !finished ? notice(html `The updater stopped. Run <code class="k-mono">toolroll update --resume</code> to continue it.`) : "";
35
+ return html `<div id="update-live" data-phase="${j.phase}" data-done="${finished ? 1 : 0}">${options.title === false ? "" : html `<p><b>${title}</b></p>`}${j.phase === "scheduled" ? note("Running work finishes first. You can close this page.") : html `<div aria-label="Update steps">${items}</div>`}${stalled}</div>`;
41
36
  }
42
37
  function outcomeHtml(j) {
43
- if (j.phase === "complete")
44
- return j.kind === "rollback" ? html `<div class="card" data-update-outcome="rolled-back"><h2>Back on ${j.to.version}</h2><p class="meta">${j.detail}</p></div>` : "";
38
+ if (j.phase === "complete") {
39
+ return j.kind === "rollback" ? settingsSection({ id: "update-outcome", title: `Back on ${j.to.version}`, body: html `<div data-update-outcome="rolled-back">${note(j.detail)}</div>` }) : "";
40
+ }
45
41
  if (j.phase === "cancelled")
46
- return html `<div class="card" data-update-outcome="cancelled"><h2>Update cancelled</h2><p class="meta">Nothing changed.</p></div>`;
42
+ return settingsSection({ id: "update-outcome", title: "Update cancelled", body: html `<div data-update-outcome="cancelled">${note("Nothing changed.")}</div>` });
47
43
  // Stopped on a finished run it can't show has ended: what is in the way and the one command, nothing more.
48
- if (j.phase === "refused" && j.waiting)
49
- return html `<div class="card" data-update-outcome="stopped"><h2>Update to ${j.to.version} stopped: run #${j.waiting.run} is in the way</h2><p>${j.waiting.on}. New work resumed.</p>${j.waiting.action ? html `<p class="meta">If nothing of it is running, run <code>${j.waiting.action}</code>, then update again.</p>` : html `<p class="meta">Update again once it has stopped.</p>`}</div>`;
50
- const title = j.phase === "refused" ? html `Didn't update to ${j.to.version}` : j.phase === "restored" ? html `Update to ${j.to.version} didn't finish` : "The update needs attention";
51
- return html `<div class="card" data-update-outcome="${j.phase}"><h2>${title}</h2><p class="${j.phase === "needs-attention" ? "update-problem" : ""}" role="alert">${j.detail}</p><details><summary>Steps</summary>${replaceMarkup(updateStepsHtml(j, false), /<h2>.*?<\/h2>/, () => html ``)}</details></div>`;
44
+ if (j.phase === "refused" && j.waiting) {
45
+ return settingsSection({ id: "update-outcome", title: `Update to ${j.to.version} stopped: run #${j.waiting.run} is in the way`,
46
+ body: html `<div data-update-outcome="stopped">${infoLine(`${j.waiting.on}. New work resumed.`)}${j.waiting.action ? note(html `If nothing of it is running, run <code class="k-mono">${j.waiting.action}</code>, then update again.`) : note("Update again once it has stopped.")}</div>` });
47
+ }
48
+ const title = j.phase === "refused" ? `Didn't update to ${j.to.version}` : j.phase === "restored" ? `Update to ${j.to.version} didn't finish` : "The update needs attention";
49
+ return settingsSection({ id: "update-outcome", title, state: html ` ${chip("Failed", "failed")}`,
50
+ body: html `<div data-update-outcome="${j.phase}">${j.phase === "needs-attention" ? notice(j.detail) : html `<p class="k-note" role="alert">${j.detail}</p>`}${disclose("Steps", updateStepsHtml(j, false, { title: false }))}</div>` });
52
51
  }
53
- export function updatesHtml(view, notice) {
54
- const note = notice.problem ? html `<p class="update-problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
52
+ export function updatesHtml(view, said) {
53
+ const answer = said.problem ? notice(said.problem) : said.said ? infoLine(said.said) : "";
55
54
  const j = view.journal;
56
55
  const active = j !== null && !runtimeUpdateTerminal(j.phase);
57
- const whatsNew = view.whatsNew === null ? "" : html `<div class="card whats-new" data-whats-new="${view.whatsNew.version}"><h2>What’s new in ${view.whatsNew.version}</h2>${view.whatsNew.notes.length > 0 ? html `<ul>${view.whatsNew.notes.map(line => html `<li>${line}</li>`)}</ul>` : ""}<p class="meta"><a href="https://github.com/ap9000/toolroll/releases/tag/v${view.whatsNew.version}" rel="noreferrer">Full release notes</a></p>${postForm("/settings/updates/seen", html `<button type="submit">Got it</button>`)}</div>`;
56
+ const whatsNew = view.whatsNew === null ? "" : settingsSection({ id: "whats-new", title: `What’s new in ${view.whatsNew.version}`,
57
+ right: postForm("/settings/updates/seen", btn("Got it", { sm: true, type: "submit" })),
58
+ body: html `<div data-whats-new="${view.whatsNew.version}">${view.whatsNew.notes.length > 0 ? view.whatsNew.notes.map(line => note(line)) : ""}${note(html `<a class="k-link" href="https://github.com/ap9000/toolroll/releases/tag/v${view.whatsNew.version}" rel="noreferrer">Full release notes</a>`)}</div>` });
58
59
  if (active) {
59
- const cancel = ["scheduled", "draining"].includes(j.phase) ? postForm("/settings/updates/cancel", html `<button type="submit">Cancel update</button>`, { attrs: { class: "update-actions" } }) : "";
60
- return html `<section class="updates">${note}<div class="card" id="update-region">${updateStepsHtml(j, view.running)}${cancel}</div><p class="meta stamp" id="update-region-stamp"></p></section>`;
60
+ const cancel = ["scheduled", "draining"].includes(j.phase) ? postForm("/settings/updates/cancel", btn("Cancel update", { sm: true, type: "submit" })) : "";
61
+ return settingsSection({ id: "update", title: "Update", state: html ` ${chip("Running", "live")}`, ...(cancel === "" ? {} : { right: cancel }),
62
+ body: joinHtml([answer, html `<div id="update-region">${updateStepsHtml(j, view.running)}</div>`, html `<p class="k-note" id="update-region-stamp" aria-live="polite"></p>`]) });
61
63
  }
62
64
  const latest = "version" in view.latest ? view.latest.version : null;
63
65
  const newer = latest !== null && newerThan(latest, view.current);
64
- const state = view.method.kind === "npx" ? html `<h2>Toolroll ${view.current}</h2><p class="meta">npx runs the latest release each time, so this is current.</p>`
65
- : view.method.kind === "source" ? html `<h2>Toolroll ${view.current}</h2><p class="meta">This runs from a source checkout. Update it with git.</p>`
66
- : newer && view.method.kind === "desktop" ? html `<h2>Toolroll ${latest} is available</h2><p class="meta">You have ${view.current}. Update it from the Toolroll app.</p>`
67
- : newer ? html `<h2>Toolroll ${latest} is available</h2><p class="meta">You have ${view.current}. Running work finishes first, and your current version is kept so you can go back.</p>`
68
- : "off" in view.latest ? html `<h2>Toolroll ${view.current}</h2><p class="meta">Update checks are off.</p>${postForm("/settings/updates/check", html `<button type="submit">Check now</button>`, { attrs: { class: "update-actions" } })}`
69
- : "pending" in view.latest ? html `<h2>Toolroll ${view.current}</h2><p class="meta">Not checked for a newer release yet.</p>${postForm("/settings/updates/check", html `<button type="submit">Check now</button>`, { attrs: { class: "update-actions" } })}`
70
- : latest === null ? html `<h2>Toolroll ${view.current}</h2><p class="meta">Couldn’t check for a newer release: ${"problem" in view.latest ? view.latest.problem : ""}</p>`
71
- : html `<h2>Toolroll ${view.current} is up to date</h2>`;
66
+ const check = postForm("/settings/updates/check", btn("Check now", { sm: true, type: "submit" }));
67
+ const state = view.method.kind === "npx" ? [`Toolroll ${view.current}`, "npx runs the latest release each time, so this is current.", ""]
68
+ : view.method.kind === "source" ? [`Toolroll ${view.current}`, "This runs from a source checkout. Update it with git.", ""]
69
+ : newer && view.method.kind === "desktop" ? [`Toolroll ${latest} is available`, `You have ${view.current}. Update it from the Toolroll app.`, ""]
70
+ : newer ? [`Toolroll ${latest} is available`, `You have ${view.current}. Running work finishes first, and your current version is kept so you can go back.`, ""]
71
+ : "off" in view.latest ? [`Toolroll ${view.current}`, "Update checks are off.", check]
72
+ : "pending" in view.latest ? [`Toolroll ${view.current}`, "Not checked for a newer release yet.", check]
73
+ : latest === null ? [`Toolroll ${view.current}`, `Couldn’t check for a newer release: ${"problem" in view.latest ? view.latest.problem : ""}`, ""]
74
+ : [`Toolroll ${view.current} is up to date`, "", ""];
72
75
  const form = newer && !["npx", "source", "desktop"].includes(view.method.kind)
73
- ? postForm("/settings/updates", html `${reauth(html `<label class="step-up">Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}<div class="update-actions"><button type="submit" name="when" value="now" class="primary">Update now</button><button type="submit" name="when" value="when-idle">When idle</button><button type="submit" name="when" value="tonight">Tonight (03:00)</button></div>`, { hidden: { version: latest } })
76
+ ? kitSectionForm({ proof: "updates", action: "/settings/updates", id: "update-form", hidden: { version: latest }, save: { label: `Update to ${latest}` }, confirm: `Update Toolroll to ${latest}`,
77
+ body: formRow("When", null, radios({ name: "when", legend: "When", current: "now", items: [
78
+ { value: "now", label: "Now", text: "Only when no work is running." },
79
+ { value: "when-idle", label: "When idle", text: "Once nothing is running." },
80
+ { value: "tonight", label: "Tonight (03:00)", text: "At 03:00 on the computer running Toolroll." }
81
+ ] }), "wide") })
74
82
  : "";
75
- const rollback = view.rollbackTo !== null ? html `<p class="meta">To go back to ${view.rollbackTo}: <code>toolroll update --rollback</code></p>` : "";
76
- return html `<section class="updates">${note}${whatsNew}${j ? outcomeHtml(j) : ""}<div class="card" data-update-state="${newer ? "available" : "current"}">${state}${form}${rollback}</div></section>`;
83
+ const rollback = view.rollbackTo !== null ? joinHtml([note(`To go back to ${view.rollbackTo}:`), codeLine("toolroll update --rollback")]) : "";
84
+ const version = settingsSection({ id: "update", title: state[0], ...(state[2] === "" ? {} : { right: state[2] }),
85
+ body: html `<div data-update-state="${newer ? "available" : "current"}">${joinHtml([answer, state[1] === "" ? "" : note(state[1]), form, rollback])}</div>` });
86
+ return joinHtml([whatsNew, j ? outcomeHtml(j) : "", version]);
77
87
  }
78
88
  export function newerThan(a, b) {
79
89
  const [x, y] = [a, b].map(v => v.split(".").map(Number));
@@ -1,8 +1,10 @@
1
1
  /** Settings → Tools: one project's MCP servers, in plain words. Secrets are
2
- * entered here and nowhere else, and never shown again. */
2
+ * entered here and nowhere else, and never shown again. Built from the revamp kit (src/design/KIT.md): each tool and
3
+ * each service is a record with one action; the password is asked only once a tool or service is picked. */
3
4
  import { type Html } from "./html.js";
4
5
  import { type FoundTool, type ProjectTool, type ToolSpec } from "./project-tools.js";
5
- export declare const TOOLS_CSS = ".tools{max-width:780px;min-width:0;overflow-wrap:anywhere}.tools .card{padding:16px 18px;margin:12px 0;min-width:0}.tools .card h2{font-size:1.05rem;margin:0;color:var(--so-ink);font-weight:600}.tools .tool-heading{display:flex;align-items:center;gap:10px}.tools .tool-heading h2{flex:1;min-width:0}.tools .tool-state{font-size:.85rem;flex-shrink:0;color:var(--so-muted)}.tools .tool-state[data-ready=\"false\"]{color:var(--so-warning)}.tools .card>p{margin:6px 0 0}.tools .card>p.tool-credential{margin-top:4px;font-size:.875rem;color:var(--so-muted)}.tools .tool-note{color:var(--so-warning)}.tools code{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font-size:.85rem}.tools form{display:grid;gap:10px;margin:0}.tools label{display:grid;gap:6px}.tools input,.tools select{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.tools button{justify-self:start;white-space:nowrap;min-height:44px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.tools details:not(.card){margin:6px 0 0;padding:0;border:0;border-radius:0;background:none;box-shadow:none}.tools details:not(.card)>summary{min-height:36px;display:flex;align-items:center;font-size:.9rem;color:var(--so-muted);cursor:pointer}.tools details:not(.card)[open]>summary{color:var(--so-ink)}.tools details.card>summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.tools summary{list-style:none;gap:10px}.tools summary::-webkit-details-marker{display:none}.tools summary::before{content:\"\";flex-shrink:0;width:6px;height:6px;margin:0 2px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transition:transform .15s}.tools details[open]>summary::before{transform:rotate(45deg)}.tools details:not(.card) form{padding:4px 0 10px}.tools .tool-facts{margin:0 0 8px;display:grid;gap:4px;font-size:.9rem}.tools .tool-facts p{margin:0}.tools .tool-controls{display:flex;gap:12px;flex-wrap:wrap;margin-top:12px}.tools .tool-found{display:flex;gap:10px;align-items:flex-start}.tools .tool-found input{flex:0 0 auto;width:18px;height:18px;margin-top:3px}.tools .tool-found span{min-width:0}.tools .problem{padding:10px 12px;margin:8px 0 0}.tools .tool-connect{scroll-margin-top:72px}.tools .connect-wanted{background:var(--so-accent);color:var(--so-on-accent);border-color:var(--so-accent)}.tools .connect-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px}.tools .connect-tile{display:grid;grid-template-columns:auto 1fr;column-gap:10px;align-items:center;justify-self:stretch;text-align:left;white-space:normal;padding:10px 12px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);color:var(--so-ink);min-height:60px;font:inherit;cursor:pointer;scroll-margin-top:96px}.tools .connect-tile:hover{border-color:var(--so-input-line);background:var(--so-raised)}.tools .connect-tile:target,.tools .connect-tile:focus-visible{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile .brand-mark{grid-row:span 2}.tools .connect-tile strong{font-weight:600;line-height:1.3}.tools .connect-tile>span:not(.brand-mark){font-size:.8rem;color:var(--so-muted);line-height:1.35;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.tools .connect-tile[data-state=\"connected\"]>span:not(.brand-mark){color:var(--so-success)}.tools .connect-local{grid-column:1/-1}.tools .connect-local>span:not(.brand-mark){-webkit-line-clamp:unset}.tools .connect-problem{margin:0}.tools .connect-problem:focus{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile:disabled{cursor:default;opacity:.6}@media(max-width:600px){.tools .connect-grid{grid-template-columns:1fr 1fr}.tools .connect-tile:not(.connect-local)>span:not(.brand-mark){display:none}.tools .connect-local>span:not(.brand-mark){grid-column:2}.tools .connect-tile{min-height:52px;padding:8px 10px}.tools .connect-tile .brand-mark{grid-row:auto}.tools input,.tools select{font-size:16px}.tools .card{padding:14px}}";
6
+ /** Kept for the console's style bundle (server/chrome.ts): the page is built from the kit and needs none of its own. */
7
+ export declare const TOOLS_CSS = "";
6
8
  export type ToolsView = {
7
9
  repo: string;
8
10
  project: string;
@@ -16,7 +18,7 @@ export type ToolsView = {
16
18
  research?: string;
17
19
  }[];
18
20
  kit: string | null;
19
- /** The service a link asked for (a kit's checklist, the lead): its own button under the password. */
21
+ /** The service a link asked for (a kit's checklist, the lead): listed first, its Connect the page's one primary. */
20
22
  wanted: string | null;
21
23
  tools: {
22
24
  tool: ProjectTool;
package/dist/tools-ui.js CHANGED
@@ -1,48 +1,97 @@
1
1
  /** Settings → Tools: one project's MCP servers, in plain words. Secrets are
2
- * entered here and nowhere else, and never shown again. */
3
- import { html, postForm, reauth } from "./html.js";
4
- import { brandMarkHtml, TILE_PHONE } from "./brand-mark.js";
2
+ * entered here and nowhere else, and never shown again. Built from the revamp kit (src/design/KIT.md): each tool and
3
+ * each service is a record with one action; the password is asked only once a tool or service is picked. */
4
+ import { html, joinHtml, postForm } from "./html.js";
5
+ import { brandMarkHtml } from "./brand-mark.js";
5
6
  import { signedIn } from "./oauth-run-token.js";
6
7
  import { credentialModeOf, toolCommandLine, toolStanding } from "./project-tools.js";
8
+ import { acts, btn, chip, codeLine, disclose, facts, formRow, icon, infoLine, input, note, notice, select, settingsRecord, settingsSection } from "./server/kit.js";
9
+ import { kitSectionForm, SECRET_OPT_OUTS } from "./settings-kit.js";
7
10
  const hidden = (data) => Object.entries(data).map(([k, v]) => html `<input type="hidden" name="${k}" value="${v}">`);
8
- export const TOOLS_CSS = `.tools{max-width:780px;min-width:0;overflow-wrap:anywhere}.tools .card{padding:16px 18px;margin:12px 0;min-width:0}.tools .card h2{font-size:1.05rem;margin:0;color:var(--so-ink);font-weight:600}.tools .tool-heading{display:flex;align-items:center;gap:10px}.tools .tool-heading h2{flex:1;min-width:0}.tools .tool-state{font-size:.85rem;flex-shrink:0;color:var(--so-muted)}.tools .tool-state[data-ready="false"]{color:var(--so-warning)}.tools .card>p{margin:6px 0 0}.tools .card>p.tool-credential{margin-top:4px;font-size:.875rem;color:var(--so-muted)}.tools .tool-note{color:var(--so-warning)}.tools code{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font-size:.85rem}.tools form{display:grid;gap:10px;margin:0}.tools label{display:grid;gap:6px}.tools input,.tools select{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.tools button{justify-self:start;white-space:nowrap;min-height:44px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.tools details:not(.card){margin:6px 0 0;padding:0;border:0;border-radius:0;background:none;box-shadow:none}.tools details:not(.card)>summary{min-height:36px;display:flex;align-items:center;font-size:.9rem;color:var(--so-muted);cursor:pointer}.tools details:not(.card)[open]>summary{color:var(--so-ink)}.tools details.card>summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.tools summary{list-style:none;gap:10px}.tools summary::-webkit-details-marker{display:none}.tools summary::before{content:"";flex-shrink:0;width:6px;height:6px;margin:0 2px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transition:transform .15s}.tools details[open]>summary::before{transform:rotate(45deg)}.tools details:not(.card) form{padding:4px 0 10px}.tools .tool-facts{margin:0 0 8px;display:grid;gap:4px;font-size:.9rem}.tools .tool-facts p{margin:0}.tools .tool-controls{display:flex;gap:12px;flex-wrap:wrap;margin-top:12px}.tools .tool-found{display:flex;gap:10px;align-items:flex-start}.tools .tool-found input{flex:0 0 auto;width:18px;height:18px;margin-top:3px}.tools .tool-found span{min-width:0}.tools .problem{padding:10px 12px;margin:8px 0 0}.tools .tool-connect{scroll-margin-top:72px}.tools .connect-wanted{background:var(--so-accent);color:var(--so-on-accent);border-color:var(--so-accent)}.tools .connect-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px}.tools .connect-tile{display:grid;grid-template-columns:auto 1fr;column-gap:10px;align-items:center;justify-self:stretch;text-align:left;white-space:normal;padding:10px 12px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);color:var(--so-ink);min-height:60px;font:inherit;cursor:pointer;scroll-margin-top:96px}.tools .connect-tile:hover{border-color:var(--so-input-line);background:var(--so-raised)}.tools .connect-tile:target,.tools .connect-tile:focus-visible{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile .brand-mark{grid-row:span 2}.tools .connect-tile strong{font-weight:600;line-height:1.3}.tools .connect-tile>span:not(.brand-mark){font-size:.8rem;color:var(--so-muted);line-height:1.35;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.tools .connect-tile[data-state="connected"]>span:not(.brand-mark){color:var(--so-success)}.tools .connect-local{grid-column:1/-1}.tools .connect-local>span:not(.brand-mark){-webkit-line-clamp:unset}.tools .connect-problem{margin:0}.tools .connect-problem:focus{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile:disabled{cursor:default;opacity:.6}${TILE_PHONE}{.tools .connect-grid{grid-template-columns:1fr 1fr}.tools .connect-tile:not(.connect-local)>span:not(.brand-mark){display:none}.tools .connect-local>span:not(.brand-mark){grid-column:2}.tools .connect-tile{min-height:52px;padding:8px 10px}.tools .connect-tile .brand-mark{grid-row:auto}.tools input,.tools select{font-size:16px}.tools .card{padding:14px}}`;
11
+ /** Kept for the console's style bundle (server/chrome.ts): the page is built from the kit and needs none of its own. */
12
+ export const TOOLS_CSS = "";
9
13
  /** The project chooser: it opens the chosen project at once; without scripts, its button does. */
10
14
  export function toolsProjectPicker(projects, chosen, keep = {}) {
11
15
  if (projects.length < 2)
12
16
  return "";
13
- const options = projects.map(p => html `<option value="${p}"${p === chosen ? html ` selected` : ""}>${p.split(/[\\/]/).filter(Boolean).at(-1) ?? p}</option>`);
14
- return html `<form class="tools" method="get" action="/settings/tools" data-autosave>${hidden(keep)}<label>Project<select name="repo" data-tools-project>${options}</select></label><button type="submit">Show project</button></form>`;
17
+ const items = projects.map(p => [p, p.split(/[\\/]/).filter(Boolean).at(-1) ?? p]);
18
+ return html `<form method="get" action="/settings/tools" class="k-acts k-picker" data-autosave>${hidden(keep)}\
19
+ ${select({ name: "repo", label: "Project", current: chosen, items, attrs: { "data-tools-project": true } })}${btn("Show project", { sm: true, type: "submit" })}</form>`;
15
20
  }
16
21
  /** Choosing another project marks every form on the page as stale before it reloads, so a tap racing the reload is refused. */
17
22
  export const TOOLS_PROJECT_SCRIPT = `(function(){var s=document.querySelector('select[data-tools-project]');if(!s)return;s.addEventListener('change',function(){document.querySelectorAll('input[name=shown]').forEach(function(i){i.value=s.value;});});})();`;
18
- const password = html `${reauth(html `<label>Your Toolroll password<input type="password" name="password" autocomplete="current-password" required></label>`)}`;
19
- function toolCard(one, base, manage, project, research = null) {
23
+ const slug = (text) => text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
24
+ /** A tool secret's own field: masked, never given a value, kept out of password managers. */
25
+ function secretField(id, label) {
26
+ return html `<input class="k-input" id="${id}" type="password" name="value" autocomplete="off" spellcheck="false" autocapitalize="none" data-so-secret \
27
+ data-1p-ignore data-lpignore="${SECRET_OPT_OUTS["data-lpignore"]}" data-bwignore data-form-type="other" required aria-label="${label}">`;
28
+ }
29
+ function toolRecord(one, base, manage, project, research = null) {
20
30
  const { tool, secretsSet } = one;
21
31
  const standing = toolStanding(tool, secretsSet);
22
32
  // How its runs get credentials, in one line; what that means is in Details.
23
33
  const mode = credentialModeOf(tool.spec, secretsSet);
24
- const credential = mode === null ? "" : html `<p class="tool-credential" data-credential="${mode.mode}">${mode.words}</p>`;
25
- // One line per secret: the disclosure is both where it stands and where it is set. A service connected by signing
26
- // in gets its tokens only by signing in again (its Connect tile): no token of it is ever pasted here.
34
+ const sub = html `${tool.spec.about}${mode === null ? "" : html ` · <span data-credential="${mode.mode}">${mode.words}</span>`}`;
35
+ // One form per secret: the disclosure is both where it stands and where it is set. A service connected by signing
36
+ // in gets its tokens only by signing in again (its Connect): no token of it is ever pasted here.
27
37
  const secrets = manage && !signedIn(tool.spec) ? tool.spec.secrets.map(secret => {
28
38
  const set = secretsSet.includes(secret.name);
29
- return html `<details><summary>${set ? `Replace ${secret.name}` : `Set ${secret.name}${secret.optional ? " (optional)" : ""}`}</summary>${postForm("/settings/tools/change", html `<label>${secret.name}<input type="password" name="value" autocomplete="off" required></label>${password}<button>Save</button>`, { hidden: { ...base, action: "secret", name: tool.name, secret: secret.name } })}</details>`;
30
- }) : "";
31
- const tested = tool.lastTest?.ok ? html `<p>Tested ${tool.lastTest.at.slice(0, 16).replace("T", " ")} UTC: ${tool.lastTest.tools.join(", ") || "it lists no tools"}</p>` : "";
32
- const details = html `<details><summary>Details</summary><div class="tool-facts">${mode?.detail == null ? "" : html `<p>${mode.detail}</p>`}<p>Starts: <code>${toolCommandLine(tool.spec)}</code></p><p>From ${tool.source} · added by ${tool.createdBy}</p>${tested}</div></details>`;
33
- const failed = tool.lastTest !== null && !tool.lastTest.ok ? html `<p class="problem" role="status">${tool.lastTest.problem ?? "The last test failed."}</p>` : "";
39
+ const id = `secret-${slug(tool.name)}-${slug(secret.name)}`;
40
+ return disclose(set ? `Replace ${secret.name}` : `Set ${secret.name}${secret.optional ? " (optional)" : ""}`, kitSectionForm({
41
+ proof: "tools-change", action: "/settings/tools/change", id, gate: true, hidden: { ...base, action: "secret", name: tool.name, secret: secret.name },
42
+ save: { label: `Save ${secret.name}` }, confirm: `Set ${secret.name} for ${tool.name} in ${project}`,
43
+ body: formRow(secret.name, "Stored privately on this computer and never shown again.", secretField(`${id}-value`, secret.name)),
44
+ }));
45
+ }) : [];
46
+ const tested = tool.lastTest?.ok ? `${tool.lastTest.at.slice(0, 16).replace("T", " ")} UTC: ${tool.lastTest.tools.join(", ") || "it lists no tools"}` : null;
47
+ const details = disclose("Details", facts([
48
+ ...(mode?.detail == null ? [] : [["Keys", mode.detail]]),
49
+ ["Starts", codeLine(toolCommandLine(tool.spec))],
50
+ ["Added", `From ${tool.source} · by ${tool.createdBy}`],
51
+ ...(tested === null ? [] : [["Last test", tested]]),
52
+ ]));
53
+ const failed = tool.lastTest !== null && !tool.lastTest.ok ? notice(tool.lastTest.problem ?? "The last test failed.") : "";
34
54
  // Connected, but research can't use it (its grant writes): said once, with the one way to fix it.
35
- const reconnect = research === null || !manage ? "" : html `<details><summary>Reconnect read-only</summary>${postForm("/settings/tools/connect", html `${password}<button>Reconnect read-only</button>`, { hidden: { ...base, service: tool.name, access: "read" } })}</details>`;
36
- const limited = research === null ? "" : html `<p class="tool-note" role="status">${research}</p>${reconnect}`;
37
- const controls = manage ? html `<div class="tool-controls">${postForm("/settings/tools/change", html `<button aria-label="Test ${tool.name} in ${project}">Test</button>`, { hidden: { ...base, action: "test", name: tool.name } })}${postForm("/settings/tools/change", html `<button class="secondary">Remove from ${project}</button>`, { hidden: { ...base, action: "remove", name: tool.name } })}</div>` : "";
38
- return html `<article class="card" id="tool-${tool.name}"><div class="tool-heading">${brandMarkHtml(tool.source === "custom" ? null : tool.name, tool.name, standing.ready)}<h2>${tool.name}</h2><span class="tool-state" data-ready="${String(standing.ready)}">${standing.words}</span></div><p>${tool.spec.about}</p>${credential}${failed}${limited}${secrets}${details}${controls}</article>`;
55
+ const reconnect = research === null || !manage ? "" : kitSectionForm({
56
+ proof: "tools-connect", action: "/settings/tools/connect", id: `reconnect-${slug(tool.name)}`, native: true, hidden: { ...base, service: tool.name, access: "read" },
57
+ save: { label: "Reconnect read-only", variant: "outline" }, confirm: `Reconnect ${tool.name} read-only for ${project}`, body: html ``,
58
+ });
59
+ const limited = research === null ? "" : html `${infoLine(research)}${reconnect}`;
60
+ const test = manage ? postForm("/settings/tools/change", btn("Test", { sm: true, type: "submit", attrs: { "aria-label": `Test ${tool.name} in ${project}` } }), { hidden: { ...base, action: "test", name: tool.name } }) : "";
61
+ const remove = manage ? postForm("/settings/tools/change", acts(btn(`Remove from ${project}`, { sm: true, variant: "danger", type: "submit" })), { hidden: { ...base, action: "remove", name: tool.name } }) : "";
62
+ return settingsRecord({
63
+ title: html `${brandMarkHtml(tool.source === "custom" ? null : tool.name, tool.name, standing.ready)}<span>${tool.name}</span>${chip(standing.words, standing.ready ? "done" : "")}`,
64
+ sub, action: test, attrs: { id: `tool-${tool.name}`, "data-ready": String(standing.ready) },
65
+ more: joinHtml([failed, limited, ...secrets, details, remove]),
66
+ });
67
+ }
68
+ /** One service to connect by signing in on its own page: one record, one Connect. The password is asked after it is picked. */
69
+ function connectRecord(one, view, base, problem) {
70
+ const mark = brandMarkHtml(one.id.replace(/-desktop$/, ""), one.label, one.state === "connected");
71
+ const attrs = { id: `connect-${one.id}`, "data-state": one.state };
72
+ if (one.state === "taken")
73
+ return settingsRecord({ title: html `${brandMarkHtml(one.id, one.label, false)}<span>${one.label}</span>`, sub: "Added another way", attrs });
74
+ const to = ` to ${view.project}`;
75
+ const connected = one.state === "connected";
76
+ const label = connected ? (one.local ? "Connect again" : "Sign in again") : "Connect";
77
+ const confirm = connected && !one.local ? `Sign in to ${one.label} again for ${view.project}` : `Connect ${one.label}${to}`;
78
+ const action = kitSectionForm({
79
+ proof: "tools-connect", action: "/settings/tools/connect", id: `connect-${one.id}-form`, native: true,
80
+ hidden: { ...base, ...(view.kit === null ? {} : { kit: view.kit }), service: one.id },
81
+ save: { label, variant: one.id === view.wanted ? "primary" : "outline" }, confirm, body: html ``,
82
+ });
83
+ return settingsRecord({ title: html `${mark}<span>${one.label}</span>${connected ? chip("Connected", "done") : ""}`, sub: connected ? "" : one.about, action, attrs, more: problem });
39
84
  }
40
- /** A service connected here, offered to the person's other projects: each button signs in again for that project (tokens are never copied). */
85
+ /** A service connected here, offered to the person's other projects: each act signs in again for that project (tokens are never copied). */
41
86
  function elsewhere(view, base) {
42
- const offers = view.connections.filter(one => one.state === "connected").flatMap(one => (view.others ?? []).filter(other => other.open.includes(one.id)).map(other => html `<button name="also" value="${`${one.id}:${other.repo}`}" class="secondary">Also connect ${one.label} to ${other.project}</button>`));
87
+ const offers = view.connections.filter(one => one.state === "connected").flatMap(one => (view.others ?? []).filter(other => other.open.includes(one.id)).map(other => kitSectionForm({
88
+ proof: "tools-connect", action: "/settings/tools/connect", id: `also-${slug(one.id)}-${slug(other.project)}`, native: true,
89
+ hidden: { ...base, ...(view.kit === null ? {} : { kit: view.kit }), also: `${one.id}:${other.repo}` },
90
+ save: { label: `Also connect ${one.label} to ${other.project}`, variant: "outline" }, confirm: `Connect ${one.label} to ${other.project}`, body: html ``,
91
+ })));
43
92
  if (offers.length === 0)
44
93
  return "";
45
- return html `<details class="tool-elsewhere"><summary>Also connect to another project</summary>${postForm("/settings/tools/connect", html `${password}<div class="tool-controls">${offers}</div>`, { hidden: { ...base, ...(view.kit === null ? {} : { kit: view.kit }) } })}</details>`;
94
+ return settingsSection({ id: "connect-elsewhere", title: "Also connect to another project", lead: "Signs in again for that project. Tokens are never copied between projects.", body: joinHtml(offers) });
46
95
  }
47
96
  export function toolsHtml(view, csrf, canManage, message = {}) {
48
97
  const manage = canManage && csrf !== "";
@@ -51,25 +100,41 @@ export function toolsHtml(view, csrf, canManage, message = {}) {
51
100
  const to = ` to ${view.project}`;
52
101
  const keptIn = view.storage === undefined || view.storage.filesOnly ? "Secrets stay on this computer" : view.storage.words.replace(/\.$/, "");
53
102
  // No keychain here: said once, plainly, where it can't be missed.
54
- const storage = view.storage?.problem !== undefined ? html `<p class="problem tool-storage" role="alert">${view.storage.problem}</p>`
55
- : view.storage?.filesOnly === true ? html `<p class="tool-storage" role="note">${view.storage.words}</p>` : "";
56
- const intro = html `<p>Builds in ${view.project} use exactly these tools. MCP servers set up elsewhere on this computer aren't used.</p><details><summary>How tools work</summary><ul><li>A tool you add reaches work you approve from now on. Approve a task again to give it a new tool.</li><li>Removing a tool takes it away from every build right away.</li><li>${keptIn}, are never shown again, and go only to the tool that needs them.</li><li>Reviewers and the lead chat never get tools. Gemini builds don't use tools yet.</li></ul></details>`;
57
- const cards = view.tools.length === 0 ? html `<p class="meta">No tools yet. Builds in this project get none.</p>` : view.tools.map(one => toolCard(one, base, manage, view.project, view.connections.find(c => c.id === one.tool.name && c.state === "connected")?.research ?? null));
58
- const fromList = view.catalog.length === 0 ? "" : postForm("/settings/tools/change", html `<label>Common tools<select name="catalog">${view.catalog.map(one => html `<option value="${one.name}">${one.label}: ${one.about}</option>`)}</select></label>${password}<button>Add${to}</button>`, { hidden: { ...base, action: "add-catalog" } });
59
- const custom = html `<details><summary>Add your own</summary>${postForm("/settings/tools/change", html `<label>Name<input name="name" pattern="[a-z0-9][a-z0-9_\\-]{0,39}" maxlength="40" required placeholder="for example: postgres"></label><label>It runs as<select name="transport"><option value="stdio">A program on this computer</option><option value="http">A web address</option></select></label><label>Command or address<input name="target" required placeholder="npx -y some-mcp-server@1.2.3, or https://…"></label><label>Secrets it needs (names, comma separated)<input name="secrets" placeholder="for example: DATABASE_URL"></label><p class="meta">For a web address, the first secret is sent as its sign-in token. Set each secret's value after adding.</p>${password}<button>Add${to}</button>`, { hidden: { ...base, action: "add-custom" } })}</details>`;
60
- // One click: sign in on the service's own page. The password is the same as for adding any tool.
61
- const tiles = view.connections.map(one => one.state === "taken"
62
- ? html `<button type="button" class="connect-tile" id="connect-${one.id}" data-state="taken" disabled>${brandMarkHtml(one.id, one.label, false)}<strong>${one.label}</strong><span>Added another way</span></button>`
63
- : html `<button name="service" value="${one.id}" class="connect-tile${one.local ? " connect-local" : ""}" id="connect-${one.id}" data-state="${one.state}" aria-label="${one.state === "connected" ? `${one.label}, connected${to}. ${one.local ? "Connect again" : "Sign in again"}` : `Connect ${one.label}${to}`}">${brandMarkHtml(one.id.replace(/-desktop$/, ""), one.label, one.state === "connected")}<strong>${one.label}</strong><span>${one.state === "connected" ? "Connected" : one.about}</span></button>`);
64
- const wanted = view.connections.find(one => one.id === view.wanted && one.state !== "taken");
65
- const first = wanted === undefined ? "" : html `<button name="service" value="${wanted.id}" class="connect-wanted">${wanted.state === "connected" && !wanted.local ? `Sign in to ${wanted.label} again for ${view.project}` : `Connect ${wanted.label}${to}`}</button>`;
103
+ const storage = view.storage?.problem !== undefined ? notice(view.storage.problem)
104
+ : view.storage?.filesOnly === true ? infoLine(view.storage.words) : "";
105
+ const how = disclose("How tools work", html `<ul class="k-note"><li>A tool you add reaches work you approve from now on. Approve a task again to give it a new tool.</li><li>Removing a tool takes it away from every build right away.</li><li>${keptIn}, are never shown again, and go only to the tool that needs them.</li><li>Reviewers and the lead chat never get tools. Gemini builds don't use tools yet.</li></ul>`);
106
+ const records = view.tools.length === 0 ? note("No tools yet. Builds in this project get none.")
107
+ : joinHtml(view.tools.map(one => toolRecord(one, base, manage, view.project, view.connections.find(c => c.id === one.tool.name && c.state === "connected")?.research ?? null)));
108
+ const tools = settingsSection({ id: "tools", title: `Tools in ${view.project}`, lead: `Builds in ${view.project} use exactly these tools. MCP servers set up elsewhere on this computer aren't used.`, body: joinHtml([storage, records, how]) });
66
109
  const connecting = manage && view.connections.length > 0;
67
- // A Connect that failed says why at its button, focused so it is heard and seen there (not only at the top).
110
+ // A Connect that failed says why at its service, focused so it is heard and seen there (not only at the top).
68
111
  const atButton = connecting && view.wanted !== null && view.connections.some(one => one.id === view.wanted) && Boolean(message.problem);
69
- const failed = atButton ? html `<p class="problem connect-problem" id="connect-problem" role="alert" tabindex="-1" autofocus>${message.problem}</p>` : "";
70
- const connect = connecting ? html `<section class="card tool-connect" id="connect" aria-labelledby="connect-heading"><h2 id="connect-heading">Connect${to}</h2>${postForm("/settings/tools/connect", html `${password}${first}${failed}<div class="connect-grid">${tiles}</div>`, { hidden: { ...base, ...(view.kit === null ? {} : { kit: view.kit }) } })}${elsewhere(view, base)}</section>` : "";
71
- const add = manage ? html `<details class="card"${view.tools.length === 0 ? html ` open` : ""}><summary>Add a tool</summary>${fromList}${custom}</details>` : "";
72
- const found = manage && view.found.length > 0 ? html `<details class="card"><summary>Found on this computer (${view.found.length})</summary><p class="meta">MCP servers your other apps use. Adding one copies its settings, and any secrets it has, into this project.</p>${postForm("/settings/tools/change", html `${view.found.map(one => html `<label class="tool-found"><input type="checkbox" name="import" value="${one.spec.name}"><span><strong>${one.spec.name}</strong> · ${one.source}<br><code>${toolCommandLine(one.spec)}</code>${one.spec.secrets.length === 0 ? "" : html `<br><span class="meta">Brings ${one.spec.secrets.map(s => s.name).join(", ")}</span>`}</span></label>`)}${password}<button>Add selected${to}</button>`, { hidden: { ...base, action: "import" } })}</details>` : "";
73
- const note = atButton ? "" : message.problem ? html `<p class="problem" role="alert">${message.problem}</p>` : message.said ? html `<p role="status">${message.said}</p>` : "";
74
- return html `<section class="tools">${note}${intro}${storage}${cards}${connect}${add}${found}</section>`;
112
+ const failed = atButton ? html `<p class="k-notice" id="connect-problem" role="alert" tabindex="-1" autofocus>${icon("alert", true)}<span>${message.problem}</span></p>` : "";
113
+ // The service a link asked for comes first; its Connect is the page's one primary.
114
+ const ordered = [...view.connections.filter(one => one.id === view.wanted), ...view.connections.filter(one => one.id !== view.wanted)];
115
+ const connect = connecting ? settingsSection({ id: "connect", title: `Connect${to}`, lead: "Sign in on the service's own page. Toolroll asks for your password once you pick one.",
116
+ body: joinHtml(ordered.map(one => connectRecord(one, view, base, one.id === view.wanted ? failed : ""))) }) : "";
117
+ const fromList = view.catalog.length === 0 ? "" : kitSectionForm({
118
+ proof: "tools-change", action: "/settings/tools/change", id: "tools-add-catalog", hidden: { ...base, action: "add-catalog" }, save: { label: `Add${to}`, variant: "outline" }, confirm: `Add a tool${to}`,
119
+ body: formRow("Common tools", null, select({ id: "tools-catalog", name: "catalog", label: "Common tools", wide: true, items: view.catalog.map(one => [one.name, `${one.label}: ${one.about}`]) }), "wide"),
120
+ });
121
+ const custom = disclose("Add your own", kitSectionForm({
122
+ proof: "tools-change", action: "/settings/tools/change", id: "tools-add-custom", hidden: { ...base, action: "add-custom" }, save: { label: `Add${to}`, variant: "outline" }, confirm: `Add your own tool${to}`,
123
+ body: joinHtml([
124
+ formRow("Name", "Lowercase letters, numbers, - and _.", input({ id: "tools-custom-name", name: "name", attrs: { pattern: "[a-z0-9][a-z0-9_\\-]{0,39}", maxlength: 40, required: true, placeholder: "for example: postgres", "aria-label": "Name" } })),
125
+ formRow("It runs as", null, select({ id: "tools-custom-transport", name: "transport", label: "It runs as", items: [["stdio", "A program on this computer"], ["http", "A web address"]] })),
126
+ formRow("Command or address", null, input({ id: "tools-custom-target", name: "target", attrs: { required: true, placeholder: "npx -y some-mcp-server@1.2.3, or https://…", "aria-label": "Command or address" } }), "wide"),
127
+ formRow("Secrets it needs", "Names, comma separated. For a web address, the first one is sent as its sign-in token. Set each value after adding.", input({ id: "tools-custom-secrets", name: "secrets", attrs: { placeholder: "for example: DATABASE_URL", "aria-label": "Secrets it needs" } })),
128
+ ]),
129
+ }));
130
+ const add = manage ? settingsSection({ id: "add", title: "Add a tool", body: joinHtml([fromList, custom]) }) : "";
131
+ const found = manage && view.found.length > 0 ? settingsSection({ id: "found", title: "Found on this computer", state: ` · ${view.found.length}`,
132
+ lead: "MCP servers your other apps use. Adding one copies its settings, and any secrets it has, into this project.",
133
+ body: kitSectionForm({
134
+ proof: "tools-change", action: "/settings/tools/change", id: "tools-import", hidden: { ...base, action: "import" }, save: { label: `Add selected${to}`, variant: "outline" }, confirm: `Add tools found on this computer${to}`,
135
+ body: html `<fieldset class="k-sopts"><legend class="k-sr">Found on this computer</legend>${view.found.map(one => html `<label class="k-sopt"><input type="checkbox" name="import" value="${one.spec.name}">\
136
+ <span><b>${one.spec.name}</b><span>${one.source} · <span class="k-mono">${toolCommandLine(one.spec)}</span>${one.spec.secrets.length === 0 ? "" : ` · brings ${one.spec.secrets.map(s => s.name).join(", ")}`}</span></span></label>`)}</fieldset>`,
137
+ }) }) : "";
138
+ const answer = atButton ? "" : message.problem ? notice(message.problem) : message.said ? infoLine(message.said) : "";
139
+ return joinHtml([answer, tools, connect, connecting ? elsewhere(view, base) : "", add, found]);
75
140
  }
@@ -37,6 +37,13 @@ export type WorkIndexItem = {
37
37
  evidence: 'recorded';
38
38
  /** The viewer's own lead took it on (lead-voice.ts): "<name> is on it.", by the name they gave it, read now. */
39
39
  lead?: string;
40
+ /** Put away (v145, task-tidy.md): by whom (a person's name, or tidy), why and when. Only the Archived view lists it. */
41
+ archived?: {
42
+ by: string;
43
+ byKind: 'person' | 'tidy';
44
+ reason: string;
45
+ at: string;
46
+ };
40
47
  };
41
48
  /** `root`: the evidence root, so a result whose checks were Off can read the release check that covers it. */
42
49
  export type WorkIndexOptions = {
@@ -129,3 +136,20 @@ export declare function workReadyForReview(store: Store, now: Date, access: Work
129
136
  run: number | null;
130
137
  }[];
131
138
  };
139
+ /** One row per task family for tidy (docs/plans/task-tidy.md): the Tasks list's own projection, so tidy's "unfinished",
140
+ * "live" and "complete" are the words the list shows. `members` are every version's task_ref ids. */
141
+ export type WorkTidyRow = {
142
+ rootId: string;
143
+ currentId: string;
144
+ title: string;
145
+ repo: string | null;
146
+ state: TaskState;
147
+ code: string;
148
+ live: boolean;
149
+ archived: boolean;
150
+ broken: boolean;
151
+ updatedAt: string;
152
+ members: number[];
153
+ memberIds: string[];
154
+ };
155
+ export declare function workTidyRows(store: Store, now: Date, access: WorkSummaryAccess): WorkTidyRow[];