toolroll 0.9.86 → 0.9.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/dist/acp-check.d.ts +9 -0
  3. package/dist/acp-check.js +40 -28
  4. package/dist/agents-board.d.ts +12 -0
  5. package/dist/agents-board.js +26 -0
  6. package/dist/approval-rules-ui.d.ts +4 -1
  7. package/dist/approval-rules-ui.js +31 -15
  8. package/dist/assignment-ui.js +1 -1
  9. package/dist/backup-ui.d.ts +5 -4
  10. package/dist/backup-ui.js +65 -32
  11. package/dist/browser/terminal.css +1 -1
  12. package/dist/browser/terminal.js +57 -54
  13. package/dist/browser/workspace.css +2 -2
  14. package/dist/browser/workspace.js +35 -70
  15. package/dist/browser-crew.js +1 -1
  16. package/dist/browser-workspace.d.ts +329 -183
  17. package/dist/browser-workspace.js +10 -9
  18. package/dist/chat-controls.d.ts +1 -1
  19. package/dist/chat-controls.js +1 -1
  20. package/dist/chat-settings-page.d.ts +29 -9
  21. package/dist/chat-settings-page.js +59 -28
  22. package/dist/check-levels-ui.d.ts +2 -1
  23. package/dist/check-levels-ui.js +67 -58
  24. package/dist/contracts/cli.d.ts +43 -0
  25. package/dist/contracts/cli.js +4 -0
  26. package/dist/contracts/console-api.d.ts +1 -1
  27. package/dist/contracts/console-api.js +1 -1
  28. package/dist/credentials-ui.d.ts +5 -3
  29. package/dist/credentials-ui.js +58 -31
  30. package/dist/design/kit.css +722 -0
  31. package/dist/design/kit.d.ts +208 -0
  32. package/dist/design/kit.js +177 -0
  33. package/dist/design/tokens.css +31 -1
  34. package/dist/discord-settings.js +8 -4
  35. package/dist/export-ui.d.ts +6 -4
  36. package/dist/export-ui.js +18 -8
  37. package/dist/flow-changed.d.ts +14 -0
  38. package/dist/flow-changed.js +53 -0
  39. package/dist/flow-gallery-ui.d.ts +41 -4
  40. package/dist/flow-gallery-ui.js +95 -63
  41. package/dist/flow-starters-ui.d.ts +3 -9
  42. package/dist/flow-starters-ui.js +12 -23
  43. package/dist/flow-unattended-words.d.ts +20 -0
  44. package/dist/flow-unattended-words.js +24 -0
  45. package/dist/flow-unattended.d.ts +9 -5
  46. package/dist/flow-unattended.js +45 -23
  47. package/dist/flows-cli.js +7 -3
  48. package/dist/flows-ui.d.ts +12 -6
  49. package/dist/flows-ui.js +141 -30
  50. package/dist/integrations-ui.d.ts +6 -4
  51. package/dist/integrations-ui.js +34 -40
  52. package/dist/integrations.d.ts +2 -2
  53. package/dist/integrations.js +4 -4
  54. package/dist/kits-ui.d.ts +8 -10
  55. package/dist/kits-ui.js +29 -33
  56. package/dist/kits.js +1 -1
  57. package/dist/knowledge-ui.js +76 -19
  58. package/dist/ledger-view.d.ts +2 -0
  59. package/dist/ledger-view.js +41 -38
  60. package/dist/models-ui.d.ts +8 -3
  61. package/dist/models-ui.js +82 -59
  62. package/dist/monitoring-ui.d.ts +4 -2
  63. package/dist/monitoring-ui.js +44 -13
  64. package/dist/needs-you.d.ts +1 -1
  65. package/dist/needs-you.js +8 -0
  66. package/dist/operate-remote-arguments.js +1 -0
  67. package/dist/operate-remote.js +1 -1
  68. package/dist/operate.d.ts +1 -1
  69. package/dist/operate.js +38 -4
  70. package/dist/passwordless.d.ts +1 -1
  71. package/dist/passwordless.js +1 -1
  72. package/dist/people-audit-ui.d.ts +2 -1
  73. package/dist/people-audit-ui.js +25 -23
  74. package/dist/policy-ui.d.ts +3 -2
  75. package/dist/policy-ui.js +27 -21
  76. package/dist/policy.d.ts +3 -0
  77. package/dist/policy.js +14 -1
  78. package/dist/project-delete-ui.d.ts +4 -2
  79. package/dist/project-delete-ui.js +135 -126
  80. package/dist/project-knowledge.d.ts +10 -10
  81. package/dist/publish.d.ts +11 -0
  82. package/dist/publish.js +16 -3
  83. package/dist/pull-request-ui.d.ts +2 -1
  84. package/dist/pull-request-ui.js +39 -28
  85. package/dist/recipe-ui.d.ts +4 -2
  86. package/dist/recipe-ui.js +14 -4
  87. package/dist/release-adapters.d.ts +3 -0
  88. package/dist/release-adapters.js +22 -2
  89. package/dist/release-cli.js +81 -9
  90. package/dist/release.d.ts +23 -0
  91. package/dist/release.js +46 -4
  92. package/dist/releases.js +1 -1
  93. package/dist/request-budget-ui.d.ts +4 -2
  94. package/dist/request-budget-ui.js +30 -19
  95. package/dist/retention-ui.d.ts +4 -3
  96. package/dist/retention-ui.js +15 -12
  97. package/dist/scope.d.ts +3 -0
  98. package/dist/scope.js +6 -0
  99. package/dist/serve.d.ts +0 -1
  100. package/dist/serve.js +51 -7
  101. package/dist/server/agents-computers.d.ts +9 -0
  102. package/dist/server/agents-computers.js +67 -0
  103. package/dist/server/chat.js +16 -16
  104. package/dist/server/chrome.d.ts +1 -1
  105. package/dist/server/chrome.js +19 -9
  106. package/dist/server/console.css +24 -12
  107. package/dist/server/flows.js +64 -36
  108. package/dist/server/home.d.ts +35 -0
  109. package/dist/server/home.js +104 -0
  110. package/dist/server/http.d.ts +3 -0
  111. package/dist/server/http.js +5 -0
  112. package/dist/server/kit-standalone.d.ts +22 -0
  113. package/dist/server/kit-standalone.js +70 -0
  114. package/dist/server/kit.d.ts +419 -0
  115. package/dist/server/kit.js +369 -0
  116. package/dist/server/pages.d.ts +2 -1
  117. package/dist/server/pages.js +213 -174
  118. package/dist/server/people-tokens.js +26 -85
  119. package/dist/server/render-history.d.ts +51 -0
  120. package/dist/server/render-history.js +228 -0
  121. package/dist/server/render-new-task.d.ts +29 -0
  122. package/dist/server/render-new-task.js +96 -0
  123. package/dist/server/render-next.d.ts +63 -0
  124. package/dist/server/render-next.js +109 -0
  125. package/dist/server/render-pages.d.ts +22 -190
  126. package/dist/server/render-pages.js +123 -575
  127. package/dist/server/render-people.d.ts +24 -3
  128. package/dist/server/render-people.js +93 -72
  129. package/dist/server/render-projects.d.ts +103 -0
  130. package/dist/server/render-projects.js +247 -0
  131. package/dist/server/render-results.d.ts +5 -24
  132. package/dist/server/render-results.js +48 -185
  133. package/dist/server/render-settings.d.ts +5 -74
  134. package/dist/server/render-settings.js +102 -493
  135. package/dist/server/render-tasks.d.ts +35 -5
  136. package/dist/server/render-tasks.js +114 -85
  137. package/dist/server/route-table.js +20 -5
  138. package/dist/server/settings.js +276 -255
  139. package/dist/server/task-views.d.ts +8 -0
  140. package/dist/server/task-views.js +85 -2
  141. package/dist/server/tasks.d.ts +8 -0
  142. package/dist/server/tasks.js +95 -40
  143. package/dist/server/terminals.js +1 -1
  144. package/dist/server/welcome.d.ts +22 -0
  145. package/dist/server/welcome.js +69 -0
  146. package/dist/settings-groups-ui.d.ts +279 -0
  147. package/dist/settings-groups-ui.js +615 -0
  148. package/dist/settings-kit.d.ts +42 -6
  149. package/dist/settings-kit.js +69 -7
  150. package/dist/setup-guide.d.ts +2 -2
  151. package/dist/skills-ui.js +59 -7
  152. package/dist/slack-settings.js +12 -5
  153. package/dist/spend-ui.d.ts +45 -26
  154. package/dist/spend-ui.js +124 -56
  155. package/dist/sso-ui.d.ts +5 -2
  156. package/dist/sso-ui.js +63 -31
  157. package/dist/storage-ui.d.ts +3 -2
  158. package/dist/storage-ui.js +56 -50
  159. package/dist/store.d.ts +112 -3
  160. package/dist/store.js +292 -20
  161. package/dist/surface.js +5 -1
  162. package/dist/teams-settings.d.ts +2 -1
  163. package/dist/teams-settings.js +24 -41
  164. package/dist/telegram-settings.d.ts +0 -4
  165. package/dist/telegram-settings.js +52 -25
  166. package/dist/toolroll-update-ui.d.ts +8 -4
  167. package/dist/toolroll-update-ui.js +55 -45
  168. package/dist/tools-ui.d.ts +5 -3
  169. package/dist/tools-ui.js +107 -42
  170. package/dist/work-index.js +11 -7
  171. package/dist/workspace-ui.d.ts +1 -1
  172. package/dist/workspace-ui.js +7 -4
  173. package/package.json +1 -1
@@ -1,13 +1,15 @@
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
8
  import { type Html } from "./html.js";
8
9
  import type { InstallMethod } from "./install-method.js";
9
10
  import { type RuntimeUpdateJournal } from "./toolroll-update.js";
10
- export declare const UPDATES_CSS: string;
11
+ /** The page is kit pieces now; nothing of the old look is left. Kept as an export for the shared stylesheet. */
12
+ export declare const UPDATES_CSS = "";
11
13
  export type UpdatesView = {
12
14
  current: string;
13
15
  /** The newest release, why it is unknown, or not asked because update checks are off. */
@@ -32,8 +34,10 @@ export type UpdatesView = {
32
34
  csrf: string;
33
35
  };
34
36
  /** The steps, each done, current, failed or waiting. Also the live region's fragment. */
35
- export declare function updateStepsHtml(j: RuntimeUpdateJournal, running: boolean): Html;
36
- export declare function updatesHtml(view: UpdatesView, notice: {
37
+ export declare function updateStepsHtml(j: RuntimeUpdateJournal, running: boolean, options?: {
38
+ title?: boolean;
39
+ }): Html;
40
+ export declare function updatesHtml(view: UpdatesView, said: {
37
41
  said?: string | null;
38
42
  problem?: string | null;
39
43
  }): Html;
@@ -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
  }
@@ -22,10 +22,10 @@ import { ASKS, NEED_ASK, NEEDS, askChipOf, failedAttemptSentence, processNeedOf,
22
22
  import { LEAD_CLAIM_SQL, LEAD_IDLE_MS } from './lead-voice.js';
23
23
  export const WORK_INDEX_PAGE_LIMIT = 40;
24
24
  export const WORK_INDEX_MAX_LIMIT = 100;
25
- const EMPTY = { all: 0, 'needs-you': 0, running: 0, completed: 0, archived: 0 };
25
+ const EMPTY = { all: 0, 'needs-you': 0, running: 0, queued: 0, completed: 0, archived: 0 };
26
26
  const NO_GROUPS = { decide: 0, review: 0, unblock: 0, building: 0, rest: 0 };
27
27
  const FAMILY_PROBLEM = 'Task history is unavailable or incomplete. This task is shown separately.';
28
- const VIEWS = ['all', 'needs-you', 'running', 'completed', 'archived'];
28
+ const VIEWS = ['all', 'needs-you', 'running', 'queued', 'completed', 'archived'];
29
29
  /** The family's current version is put away (v145): it waits on nobody and only the Archived view lists it. */
30
30
  const ARCHIVED = `EXISTS(SELECT 1 FROM task_archive ta WHERE ta.task_ref=c.ref_id)`;
31
31
  const n = (row, key) => Number(row[key] ?? 0);
@@ -333,11 +333,13 @@ function parameters(now, access, options, custody) {
333
333
  $leadOf: access.principal === 'operator' && access.viewer != null && access.viewer !== '' ? `lead for ${access.viewer}` : null };
334
334
  }
335
335
  function counts(row) {
336
- return { all: n(row, 'all_count'), 'needs-you': n(row, 'needs_count'), running: n(row, 'running_count'), completed: n(row, 'completed_count'), archived: n(row, 'archived_count') };
336
+ return { all: n(row, 'all_count'), 'needs-you': n(row, 'needs_count'), running: n(row, 'running_count'), queued: n(row, 'queued_count'), completed: n(row, 'completed_count'), archived: n(row, 'archived_count') };
337
337
  }
338
338
  // Put-away families (v145) count only as archived.
339
+ // Queued: waits on someone or something else (rank 2), not a person and not a build of its own family.
340
+ const QUEUED = `rank=2 AND family_running=0 AND archived=0`;
339
341
  const TOTALS = `COALESCE(SUM(archived=0),0) all_count,COALESCE(SUM(needs),0) needs_count,COALESCE(SUM(family_running AND archived=0),0) running_count,`
340
- + `COALESCE(SUM(code='complete' AND archived=0),0) completed_count,COALESCE(SUM(archived),0) archived_count`;
342
+ + `COALESCE(SUM(${QUEUED}),0) queued_count,COALESCE(SUM(code='complete' AND archived=0),0) completed_count,COALESCE(SUM(archived),0) archived_count`;
341
343
  const GROUP_TOTALS = `COALESCE(SUM(rank=0 AND ask_rank=0 AND archived=0),0) decide_count,COALESCE(SUM(rank=0 AND ask_rank=1 AND archived=0),0) review_count,COALESCE(SUM(rank=0 AND ask_rank=2 AND archived=0),0) unblock_count,`
342
344
  + `COALESCE(SUM(rank=1 AND archived=0),0) building_count,COALESCE(SUM(rank>1 AND archived=0),0) rest_count`;
343
345
  function groupCounts(row) {
@@ -389,7 +391,7 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
389
391
  const view = VIEWS.includes(options.view ?? 'all') ? options.view ?? 'all' : 'all';
390
392
  const limit = Number.isFinite(options.limit) ? Math.max(1, Math.min(WORK_INDEX_MAX_LIMIT, Math.floor(options.limit))) : WORK_INDEX_PAGE_LIMIT;
391
393
  const scope = cursorScope(access, options, view), cursor = readCursor(options.cursor, scope);
392
- const filter = view === 'archived' ? 'archived=1' : view === 'needs-you' ? 'needs=1' : view === 'running' ? 'family_running=1 AND archived=0' : view === 'completed' ? "code='complete' AND archived=0" : 'archived=0';
394
+ const filter = view === 'archived' ? 'archived=1' : view === 'needs-you' ? 'needs=1' : view === 'running' ? 'family_running=1 AND archived=0' : view === 'queued' ? QUEUED : view === 'completed' ? "code='complete' AND archived=0" : 'archived=0';
393
395
  const rows = prepared(store, `${projection}, selected_page AS MATERIALIZED (
394
396
  SELECT * FROM ranked WHERE ($taskId IS NOT NULL OR ${filter}) AND ($taskId IS NULL OR root_ref IN (SELECT root_ref FROM members WHERE id=$taskId)) AND ($roots IS NULL OR root_id IN (SELECT value FROM json_each($roots))) AND ($cursorRoot=0 OR
395
397
  ${ORDER}>$cursorRank OR (${ORDER}=$cursorRank AND (sort_at<$cursorAt OR (sort_at=$cursorAt AND root_ref<$cursorRoot))))
@@ -399,8 +401,8 @@ function readWorkIndexPage(store, now, access, options, taskId = null) {
399
401
  SUM(state='done' AND family_running=0 AND archived=0 AND updated_at>=$recent) done_recently FROM ranked GROUP BY repo
400
402
  )
401
403
  SELECT 0 record_kind,totals.*,(SELECT json_group_array(json_object('repo',repo,'all_count',all_count,'needs_count',needs_count,
402
- 'running_count',running_count,'completed_count',completed_count,'archived_count',archived_count,'queued',queued,'done_recently',done_recently)) FROM project_totals) row_json FROM totals
403
- UNION ALL SELECT 1,0,0,0,0,0,0,0,0,0,0,json_object('archived',archived,'archived_by',(SELECT by FROM task_archive WHERE task_ref=page.ref_id),
404
+ 'running_count',running_count,'queued_count',queued_count,'completed_count',completed_count,'archived_count',archived_count,'queued',queued,'done_recently',done_recently)) FROM project_totals) row_json FROM totals
405
+ UNION ALL SELECT 1,0,0,0,0,0,0,0,0,0,0,0,json_object('archived',archived,'archived_by',(SELECT by FROM task_archive WHERE task_ref=page.ref_id),
404
406
  'archived_kind',(SELECT by_kind FROM task_archive WHERE task_ref=page.ref_id),'archived_reason',(SELECT reason FROM task_archive WHERE task_ref=page.ref_id),
405
407
  'archived_at',(SELECT at FROM task_archive WHERE task_ref=page.ref_id),'root_ref',root_ref,'root_id',root_id,'id',id,'root_title',root_title,'repo',repo,
406
408
  'state',state,'created_at',created_at,'family_updated',family_updated,'version_count',version_count,'earlier_active',earlier_active,
@@ -716,6 +718,8 @@ function itemOf(row, principal, probe, leadName, readiness, readable = false) {
716
718
  views.push('needs-you');
717
719
  if (archived === null && running)
718
720
  views.push('running');
721
+ if (archived === null && !need && !running && n(row, 'rank') === 2)
722
+ views.push('queued');
719
723
  if (archived === null && code === 'complete')
720
724
  views.push('completed');
721
725
  return { rootId: String(row['root_id']), activeTaskId: id, title: String(row['root_title']), repo: s(row, 'repo'),
@@ -26,7 +26,7 @@ import { type ChecksFact } from "./task-status.js";
26
26
  import { type Html } from "./html.js";
27
27
  /** The Work destination's views — shortcuts over the same rows, never a
28
28
  * persisted state. All is the default. */
29
- export type WorkView = "all" | "needs-you" | "running" | "completed" | "archived";
29
+ export type WorkView = "all" | "needs-you" | "running" | "queued" | "completed" | "archived";
30
30
  export declare const WORK_VIEWS: readonly {
31
31
  key: WorkView;
32
32
  label: string;
@@ -25,12 +25,13 @@ import { html, postForm } from "./html.js";
25
25
  export const WORK_VIEWS = [
26
26
  { key: "all", label: "All", hint: "Every task in view, most urgent first.", empty: "Nothing is in progress. Describe work in chat or add a task, and it appears here." },
27
27
  { key: "needs-you", label: "Needs you", hint: "Tasks waiting on an answer, approval, or your inspection.", empty: "Nothing needs you right now. Queued and running work continues on its own." },
28
- { key: "running", label: "Building", hint: "Attempts a builder owns right now.", empty: "Nothing is building right now. Approved tasks start when a builder with capacity is connected." },
29
- { key: "completed", label: "Complete", hint: "Tasks the lead or user has marked complete. Recorded check results remain available.", empty: "No tasks have been marked complete in this view." },
28
+ { key: "running", label: "Running", hint: "Attempts a builder owns right now.", empty: "Nothing is running right now. Approved tasks start when a builder with capacity is connected." },
29
+ { key: "queued", label: "Queued", hint: "Tasks that start on their own: waiting for a builder, a dependency or a retry.", empty: "Nothing is queued." },
30
+ { key: "completed", label: "Finished", hint: "Tasks the lead or user has marked complete. Recorded check results remain available.", empty: "Nothing has finished in this view yet." },
30
31
  { key: "archived", label: "Archived", hint: "Tasks put away by you or by tidy. Their history and results are kept.", empty: "Nothing is put away." },
31
32
  ];
32
33
  export function parseWorkView(raw) {
33
- return raw === "needs-you" || raw === "running" || raw === "completed" || raw === "archived" ? raw : "all";
34
+ return raw === "needs-you" || raw === "running" || raw === "queued" || raw === "completed" || raw === "archived" ? raw : "all";
34
35
  }
35
36
  /** The review facts from the store's bounded retry projection — the same
36
37
  * reading `diagnoseTaskDispatch` makes, so every surface starts from one
@@ -469,6 +470,8 @@ function workStatusWordsOf(facts, resultDisplay) {
469
470
  views.push("needs-you");
470
471
  if (running)
471
472
  views.push("running");
473
+ if (!needs && !running && facts.state === "queued")
474
+ views.push("queued");
472
475
  if (facts.state === "done")
473
476
  views.push("completed");
474
477
  const dispatchWords = (tone, rank, action) => ({
@@ -526,7 +529,7 @@ function workStatusWordsOf(facts, resultDisplay) {
526
529
  }
527
530
  /** The counts each view tab wears — from the same rows the page lists. */
528
531
  export function workCounts(rows) {
529
- const counts = { all: 0, "needs-you": 0, running: 0, completed: 0, archived: 0 };
532
+ const counts = { all: 0, "needs-you": 0, running: 0, queued: 0, completed: 0, archived: 0 };
530
533
  for (const row of rows)
531
534
  for (const view of row.views)
532
535
  counts[view] += 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.86",
3
+ "version": "0.9.88",
4
4
  "description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
5
5
  "directories": {
6
6
  "doc": "docs"