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
@@ -327,7 +327,7 @@ function targets(io) {
327
327
  const list = [];
328
328
  list.push({
329
329
  key: "telegram", group: "chat", name: "Telegram", configured: telegram !== null, account: null,
330
- setup: place("/settings#telegram-token"), fix: place("/settings#telegram-token"), usedBy: chatUses("telegram"), activity: telegramActivity(store),
330
+ setup: place("/settings/alerts#telegram-token"), fix: place("/settings/alerts#telegram-token"), usedBy: chatUses("telegram"), activity: telegramActivity(store),
331
331
  probe: telegram === null ? null : check => probeTelegram(check, telegram.token),
332
332
  });
333
333
  list.push({
@@ -371,7 +371,7 @@ function targets(io) {
371
371
  : { okAt: str(mailWatch["updated_at"]), error: null, errorAt: null, failing: false };
372
372
  list.push({
373
373
  key: "email", group: "mail", name: "Email", configured: email !== null || google !== null, account: email?.from ?? google?.address ?? null,
374
- setup: place("/settings#email"), fix: place("/settings#email"), usedBy: [...flowWords(emailUses)], activity: merge(triggerActivity(emailUses), watchActivity),
374
+ setup: place("/settings/alerts#email"), fix: place("/settings/alerts#email"), usedBy: [...flowWords(emailUses)], activity: merge(triggerActivity(emailUses), watchActivity),
375
375
  probe: email === null ? null : check => probeEmail(check, { host: email.host, port: email.port, secure: email.secure }, email.imap),
376
376
  });
377
377
  // Each project's MCP tools: the server starts and lists its tools.
@@ -432,7 +432,7 @@ function targets(io) {
432
432
  for (const provider of AGENTS) {
433
433
  list.push({
434
434
  key: `agent:${provider}`, group: "agents", name: providerName(provider), configured: true, account: null,
435
- setup: place("/settings#providers"), fix: place("/settings#providers"), usedBy: ["Tasks"], activity: NO_ACTIVITY,
435
+ setup: place("/settings/agents#providers"), fix: place("/settings/agents#providers"), usedBy: ["Tasks"], activity: NO_ACTIVITY,
436
436
  probe: check => probeAgent(check, provider),
437
437
  });
438
438
  }
@@ -458,7 +458,7 @@ function combine(target, row) {
458
458
  if (failing !== null) {
459
459
  return { ...base, ...common, state: "broken", action: { kind: "fix", label: "Fix", words: scrubIntegrationText(failing ?? "The last check failed."), ...target.fix } };
460
460
  }
461
- return { ...base, ...common, state: "connected", action: { kind: "test", label: "Send test" } };
461
+ return { ...base, ...common, state: "connected", action: { kind: "test", label: "Check connection" } };
462
462
  }
463
463
  /**
464
464
  * youiyoux change requests, one row per project that uses youiyoux (youiyoux-intake.ts): Connected with how many of
package/dist/kits-ui.d.ts CHANGED
@@ -1,18 +1,16 @@
1
1
  /**
2
- * Starter kits pages: the gallery — each kit's promise, what it sets
3
- * up, one button — and a kit's own page: its checklist, with the next step on
4
- * each line (connect email, connect a tool, try a sample card).
2
+ * Starter kits: the "With an agent" rows of New flow (each kit's promise, what it sets up, one button to its page)
3
+ * and a kit's own page, a reading page: what it sets up, then its checklist with the next step on each line
4
+ * (connect email, connect a tool, try a sample card).
5
5
  */
6
6
  import type { Store } from "./store.js";
7
7
  import { type Kit } from "./kits.js";
8
8
  import { type Html } from "./html.js";
9
9
  export declare const KITS_CSS: string;
10
- /** The gallery: every kit, set up in a project with one button (or opened, when it's already there). */
11
- export declare function kitsGalleryHtml(store: Store, projects: readonly string[], projectName: (repo: string) => string, canSetUp: boolean, notice: {
12
- problem?: string | null;
13
- }): Html;
14
- /** A kit's own page: what's set up, and the next step on each line. */
15
- export declare function kitPageHtml(store: Store, kit: Kit, repo: string, dir: string | null, canSetUp: boolean, notice: {
10
+ /** "With an agent": every kit as a row; Set it up (or Open, once it's there) goes to the kit's page for `repo`. */
11
+ export declare function kitRowsHtml(store: Store, repo: string | null): Html;
12
+ /** A kit's own page: what it sets up, Set up (once), then the next step on each line. */
13
+ export declare function kitPageHtml(store: Store, kit: Kit, repo: string, dir: string | null, canSetUp: boolean, notice_: {
16
14
  said?: string | null;
17
15
  problem?: string | null;
18
- }): Html;
16
+ }, projectName?: (repo: string) => string): Html;
package/dist/kits-ui.js CHANGED
@@ -1,46 +1,42 @@
1
1
  import { kitChecklist, kitInstalled, KITS } from "./kits.js";
2
2
  import { SUBAGENT_TEMPLATES } from "./subagents.js";
3
3
  import { html, postForm, reauth } from "./html.js";
4
- const CHECK = html `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>`;
5
- const RING = html `<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8"/></svg>`;
6
- export const KITS_CSS = `.kits{max-width:960px;min-width:0}.kits-lede{color:var(--so-muted);margin:.25rem 0 1.5rem;max-width:40rem}.kit-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}` +
7
- `.kit{display:flex;flex-direction:column;gap:10px;padding:20px;border:1px solid var(--so-line);border-radius:12px;background:var(--so-paper)}.kit h2{font-size:1.1rem;margin:0}.kit p{margin:0}.kit .kit-promise{line-height:1.5}` +
8
- `.kit .kit-gets{font-size:.85rem;color:var(--so-muted);line-height:1.5}.kit form{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end;margin-top:auto;padding-top:6px}.kit form label{display:grid;gap:4px;font-size:.85rem;flex:1 1 140px}` +
9
- `.kit button,.kit .kit-open{min-height:44px}.kit .kit-open{display:inline-flex;align-items:center;margin-top:auto;font-weight:600}` +
10
- `.kit-steps{list-style:none;padding:0;margin:1rem 0;display:grid;gap:2px;max-width:44rem}.kit-steps li{display:flex;align-items:center;gap:12px;padding:12px 14px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper)}` +
11
- `.kit-steps svg{flex:none;width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round}.kit-steps li[data-done="true"] svg{color:var(--so-success)}.kit-steps li[data-done="false"] svg{color:var(--so-muted)}` +
12
- `.kit-steps .kit-step-said{flex:1;min-width:0}.kit-steps li[data-done="true"] .kit-step-said{color:var(--so-muted)}.kit-steps form{margin:0}.kit-steps button,.kit-steps .kit-step-go{min-height:40px;white-space:nowrap}` +
13
- `.kit-steps .kit-step-go{display:inline-flex;align-items:center;padding:0 12px;border:1px solid var(--so-line);border-radius:8px;text-decoration:none;font-size:.9rem}` +
14
- `@media(max-width:600px){.kit-steps li{flex-wrap:wrap}.kit-steps .kit-step-said{flex-basis:calc(100% - 40px)}.kit form select{font-size:16px}}`;
4
+ import { templateRow } from "./flow-gallery-ui.js";
5
+ import { acts, btn, count, doc, docHeader, dock, facts, group, icon, infoLine, input, note, notice, phoneHeader } from "./server/kit.js";
6
+ export const KITS_CSS = `.fl-kit-steps{list-style:none;padding:0;margin:0;display:grid;border-top:1px solid var(--so-line)}` +
7
+ `.fl-kit-steps li{display:flex;align-items:center;gap:12px;min-height:48px;padding:8px 0;border-bottom:1px solid var(--so-line);min-width:0}` +
8
+ `.fl-kit-steps li>.k-i{flex:none;color:var(--so-muted)}.fl-kit-steps li[data-done="true"]>.k-i{color:var(--so-done-ink)}.fl-kit-ring{flex:none;width:14px;height:14px;margin:0 1px;border:1.5px solid var(--so-field-line);border-radius:50%;box-sizing:border-box}` +
9
+ `.fl-kit-said{flex:1;min-width:0;font-size:13px;overflow-wrap:anywhere}.fl-kit-steps li[data-done="true"] .fl-kit-said{color:var(--so-muted)}.fl-kit-steps form{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:0}.fl-kit-steps .k-input{width:180px}` +
10
+ `@media(max-width:760px){.fl-kit-steps li{flex-wrap:wrap}.fl-kit-said{flex-basis:calc(100% - 40px)}.fl-kit-steps form,.fl-kit-steps .k-input{width:100%}}`;
15
11
  const subagentWords = (kit) => { const template = SUBAGENT_TEMPLATES.find(one => one.id === kit.subagent.template); return template === undefined ? kit.subagent.handle : template.label.toLowerCase(); };
16
- /** The gallery: every kit, set up in a project with one button (or opened, when it's already there). */
17
- export function kitsGalleryHtml(store, projects, projectName, canSetUp, notice) {
18
- const cards = KITS.map(kit => {
19
- const here = projects.filter(repo => kitInstalled(store, kit, repo) !== null);
20
- const open = projects.length === 1 && here.length === 1;
21
- const name = kit.subagent.handle.charAt(0).toUpperCase() + kit.subagent.handle.slice(1);
22
- const gets = `${name} (${subagentWords(kit)}) · the ${kit.flowName} flow${kit.tools.length === 0 ? "" : ` · works with ${kit.tools.map(one => one.label).join(" and ")}`}`;
23
- const action = open ? html `<a class="kit-open" href="/kits/${kit.id}?repo=${encodeURIComponent(projects[0])}">Open ${kit.name} →</a>`
24
- : !canSetUp || projects.length === 0 ? ""
25
- : postForm(`/kits/${kit.id}/setup`, html `${projects.length === 1 ? ""
26
- : html `<label>Project<select name="repo">${projects.map(repo => html `<option value="${repo}">${projectName(repo)}${here.includes(repo) ? " (set up)" : ""}</option>`)}</select></label>`}<button>Set it up</button>`, { hidden: { repo: projects.length === 1 ? projects[0] : null } });
27
- return html `<article class="kit" data-kit="${kit.id}"><h2>${kit.name}</h2><p class="kit-promise">${kit.promise}</p><p class="kit-gets">${gets}</p>${action}</article>`;
12
+ const agentName = (kit) => kit.subagent.handle.charAt(0).toUpperCase() + kit.subagent.handle.slice(1);
13
+ const kitHref = (kit, repo) => `/kits/${kit.id}?repo=${encodeURIComponent(repo)}`;
14
+ /** "With an agent": every kit as a row; Set it up (or Open, once it's there) goes to the kit's page for `repo`. */
15
+ export function kitRowsHtml(store, repo) {
16
+ const rows = KITS.map(kit => {
17
+ const here = repo !== null && kitInstalled(store, kit, repo) !== null;
18
+ const gets = `${agentName(kit)} (${subagentWords(kit)}) and the ${kit.flowName} flow${kit.tools.length === 0 ? "" : ` · works with ${kit.tools.map(one => one.label).join(" and ")}`}`;
19
+ return templateRow({ name: kit.name, promise: kit.promise, line: gets, attrs: html ` data-kit="${kit.id}"`,
20
+ action: repo === null ? "" : btn(here ? "Open" : "Set it up", { sm: true, href: kitHref(kit, repo), attrs: { "aria-label": `${here ? "Open" : "Set up"} ${kit.name}` } }) });
28
21
  });
29
- return html `<section class="kits">${notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : ""}<p class="kits-lede">A working setup in one click: a subagent, the flow it works, and what to connect next. Nothing goes out without you approving it.</p><div class="kit-grid">${cards}</div></section>`;
22
+ return html `${group("With an agent", count(KITS.length))}<div data-group="kits">${rows}</div>`;
30
23
  }
31
- /** A kit's own page: what's set up, and the next step on each line. */
32
- export function kitPageHtml(store, kit, repo, dir, canSetUp, notice) {
24
+ /** A kit's own page: what it sets up, Set up (once), then the next step on each line. */
25
+ export function kitPageHtml(store, kit, repo, dir, canSetUp, notice_, projectName = one => one) {
33
26
  const steps = kitChecklist(store, kit, repo, dir);
34
27
  const set = kitInstalled(store, kit, repo);
28
+ const back = `/flows/new?group=kits&repo=${encodeURIComponent(repo)}`;
35
29
  const rows = steps.map(step => {
36
- const go = step.done ? (step.href === null ? "" : html `<a class="kit-step-go" href="${step.href}">Open</a>`)
30
+ const go = step.done ? (step.href === null ? "" : btn("Open", { sm: true, href: step.href }))
37
31
  : step.action === "sample" || step.action === "github"
38
- ? (canSetUp ? postForm(`/kits/${kit.id}/${step.action}`, step.action === "sample" ? html `<button>Try it</button>`
32
+ ? (canSetUp ? postForm(`/kits/${kit.id}/${step.action}`, step.action === "sample" ? btn("Try it", { sm: true, type: "submit" })
39
33
  // Issues from anyone come in: it takes your password (an ordinary step-up; a session does under Passwordless).
40
- : html `${reauth(html `<input type="password" name="password" autocomplete="current-password" aria-label="Your password" placeholder="Your password">`)}<button class="secondary">Bring them in</button>`, { hidden: { repo } }) : "")
41
- : step.href === null ? "" : html `<a class="kit-step-go" href="${step.href}">${step.action === "connect" ? "Connect" : "Set up"}</a>`;
42
- return html `<li data-step="${step.id}" data-done="${String(step.done)}">${step.done ? CHECK : RING}<span class="kit-step-said">${step.said}</span>${go}</li>`;
34
+ : html `${reauth(input({ id: `fl-kit-${step.id}-password`, name: "password", type: "password", attrs: { autocomplete: "current-password", "aria-label": "Your password", placeholder: "Your password" } }))}${btn("Bring them in", { sm: true, type: "submit" })}`, { hidden: { repo } }) : "")
35
+ : step.href === null ? "" : btn(step.action === "connect" ? "Connect" : "Set up", { sm: true, href: step.href });
36
+ return html `<li data-step="${step.id}" data-done="${String(step.done)}">${step.done ? icon("check", true) : html `<span class="fl-kit-ring" aria-hidden="true"></span>`}<span class="fl-kit-said">${step.said}</span>${go}</li>`;
43
37
  });
44
- const setUp = set === null && canSetUp ? postForm(`/kits/${kit.id}/setup`, html `<button>Set up ${kit.name}</button>`, { hidden: { repo } }) : "";
45
- return html `<section class="kits">${notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : ""}${notice.said ? html `<p class="said" role="status">${notice.said}</p>` : ""}<p class="kits-lede">${kit.promise}</p>${setUp}${rows.length === 0 ? "" : html `<ol class="kit-steps">${rows}</ol>`}<p class="meta"><a href="/kits">All starter kits</a></p></section>`;
38
+ const setUp = set === null && canSetUp ? postForm(`/kits/${kit.id}/setup`, acts(btn(`Set up ${kit.name}`, { variant: "primary", type: "submit", docked: true })), { hidden: { repo }, attrs: { id: "fl-kit-setup" } }) : "";
39
+ const about = facts([["Project", projectName(repo)], ["Agent", `${agentName(kit)} (${subagentWords(kit)})`], ["Flow", kit.flowName], ...(kit.tools.length === 0 ? [] : [["Works with", kit.tools.map(one => one.label).join(" and ")]])]);
40
+ return html `<div class="fl-kit" data-kit-page data-kit="${kit.id}">${phoneHeader(kit.name, { back })}${doc(docHeader({ title: kit.name, back: [back, "New flow"], meta: kit.promise, same: true }), notice_.problem ? notice(notice_.problem) : "", notice_.said ? infoLine(notice_.said) : "", about, set === null ? note("Nothing goes out without you approving it.") : "", setUp, rows.length === 0 ? "" : html `<section class="k-sec" aria-labelledby="fl-kit-next"><h3 id="fl-kit-next">Next steps</h3><ol class="fl-kit-steps">${rows}</ol></section>`)}\
41
+ ${set === null && canSetUp ? dock(btn(`Set up ${kit.name}`, { variant: "primary", type: "submit", attrs: { form: "fl-kit-setup" } })) : ""}</div>`;
46
42
  }
package/dist/kits.js CHANGED
@@ -171,7 +171,7 @@ export function kitChecklist(store, kit, repo, dir) {
171
171
  if (kit.email === true) {
172
172
  // (flow-actions' sendingReady, without its imports: kits load beside the chat actions.)
173
173
  const ready = (googleConnected(dir) !== null || readEmailSettings(dir) !== null) && mailboxReady(dir);
174
- steps.push({ id: "email", done: ready, said: ready ? "Email is connected" : "Connect your email so replies go out and answers come back", href: "/settings#email" });
174
+ steps.push({ id: "email", done: ready, said: ready ? "Email is connected" : "Connect your email so replies go out and answers come back", href: "/settings/alerts#email" });
175
175
  }
176
176
  if (kit.github === true) {
177
177
  const github = githubRepoOf(repo);
@@ -1,5 +1,6 @@
1
1
  import { STALE_KNOWLEDGE } from './project-knowledge.js';
2
- import { html, postForm } from './html.js';
2
+ import { html, joinHtml, postForm } from './html.js';
3
+ import { acts, btn, codeLine, disclose, formRow, input, note, notice, settingsLink, settingsRecord, settingsSection, textarea } from './server/kit.js';
3
4
  // Hidden fields as they always were: a missing value is an empty field.
4
5
  const fields = (data) => Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v ?? '')]));
5
6
  /** A section as a frame (after ReUI's Frame, MIT): its heading on the raised well, its body one paper panel. The
@@ -12,18 +13,46 @@ export function knowledgeHtml(view, csrf, canManage, draft, error) {
12
13
  const manage = canManage && !!csrf;
13
14
  const base = { repo: view.repo, identity: view.identity, revision: view.revision };
14
15
  const form = (action, body, extra = {}) => postForm('/settings/knowledge/change', body, { hidden: fields({ ...base, action, ...extra }) });
15
- const referenceFields = (d) => html `<label>Name<input name="title" maxlength="120" value="${d.title}" required></label><label>Reference text<textarea name="content" rows="5" maxlength="12000">${d.content}</textarea></label><details${d.path ? html ` open` : ''}><summary>Use a project document</summary><label>Committed .md or .txt path<input name="path" value="${d.path}" placeholder="docs/design-guide.md"></label><p class="meta">Uses the committed file instead of the text above. Save again to refresh it.</p></details><button type="submit">Save reference</button>`;
16
- const refs = view.knowledge.references.map(ref => html `<details class="card"${draft?.id === ref.id ? html ` open` : ''}><summary><strong>${ref.title}</strong></summary>${ref.path ? html `<span class="meta knowledge-source">${ref.path} · ${ref.sourceRevision?.slice(0, 7)}</span>` : ''}<pre>${ref.content}</pre>${manage ? html `<details${draft?.id === ref.id ? html ` open` : ''}><summary>Edit reference</summary>${form('save', referenceFields(draft?.id === ref.id ? draft : { title: ref.title, content: ref.path ? '' : ref.content, path: ref.path ?? '' }), { id: ref.id })}<details><summary>Remove reference</summary>${form('remove', html `<p>Remove from future tasks. Previous run context stays available.</p><button>Remove reference</button>`, { id: ref.id })}</details></details>` : ''}</details>`);
17
- const history = view.history.map(h => html `<li><a class="button-link" href="/settings/knowledge?repo=${encodeURIComponent(view.repo)}&version=${h.revision}">Version ${h.revision}</a> · ${h.actor} · ${h.at.slice(0, 16).replace('T', ' ')} UTC</li>`);
16
+ const referenceFields = (d, key) => joinHtml([
17
+ formRow('Name', null, input({ id: `ref-${key}-title`, name: 'title', value: d.title ?? '', attrs: { maxlength: 120, required: true, 'aria-label': 'Name' } }), 'wide'),
18
+ formRow('Reference text', null, textarea({ id: `ref-${key}-content`, name: 'content', rows: 5, value: d.content ?? '', attrs: { maxlength: 12000, 'aria-label': 'Reference text' } }), 'top'),
19
+ disclose('Use a project document', formRow('Committed .md or .txt path', 'Uses the committed file instead of the text above. Save again to refresh it.', input({ id: `ref-${key}-path`, name: 'path', value: d.path ?? '', attrs: { placeholder: 'docs/design-guide.md', 'aria-label': 'Committed .md or .txt path' } }), 'wide'), Boolean(d.path)),
20
+ acts(btn('Save reference', { sm: true, type: 'submit' })),
21
+ ]);
22
+ const refs = view.knowledge.references.map(ref => settingsRecord({
23
+ title: ref.title,
24
+ sub: ref.path ? html `<span class="k-mono">${ref.path} · ${ref.sourceRevision?.slice(0, 7)}</span>` : '',
25
+ more: joinHtml([
26
+ disclose('Read reference', html `<pre class="k-scode">${ref.content}</pre>`, draft?.id === ref.id),
27
+ manage ? disclose('Edit reference', joinHtml([
28
+ form('save', referenceFields(draft?.id === ref.id ? draft : { title: ref.title, content: ref.path ? '' : ref.content, path: ref.path ?? '' }, String(ref.id)), { id: ref.id }),
29
+ disclose('Remove reference', form('remove', joinHtml([note('Remove from future tasks. Previous run context stays available.'), acts(btn('Remove reference', { sm: true, variant: 'danger', type: 'submit' }))]), { id: ref.id })),
30
+ ]), draft?.id === ref.id) : '',
31
+ ]),
32
+ }));
33
+ const history = view.history.map(h => settingsLink({ href: `/settings/knowledge?repo=${encodeURIComponent(view.repo)}&version=${h.revision}`, title: `Version ${h.revision}`, status: `${h.actor} · ${h.at.slice(0, 16).replace('T', ' ')} UTC` }));
34
+ const problem = error ? notice(error) : '';
18
35
  if (view.stale) {
19
36
  // One notice, the saved knowledge on demand, one approver action. Editing waits until it is applied.
20
37
  const saved = view.stale.knowledge;
21
- const review = html `<details><summary>Review saved knowledge</summary><pre class="knowledge-instructions">${saved.instructions || 'No project instructions.'}</pre>${saved.references.map(ref => html `<details><summary>${ref.title}</summary><pre>${ref.content}</pre></details>`)}</details>`;
22
- return html `<section class="knowledge knowledge-library">${error ? html `<p class="problem" role="alert">${error}</p>` : ''}<section class="card knowledge-stale"><p role="status">${STALE_KNOWLEDGE}.</p>${review}${manage ? form('apply', html `<button>Apply saved knowledge</button>`) : ''}</section></section>`;
38
+ const review = disclose('Review saved knowledge', joinHtml([html `<pre class="k-scode">${saved.instructions || 'No project instructions.'}</pre>`, ...saved.references.map(ref => disclose(ref.title, html `<pre class="k-scode">${ref.content}</pre>`))]));
39
+ return joinHtml([problem, settingsSection({ id: 'knowledge-stale', title: 'Saved knowledge', body: joinHtml([
40
+ html `<p class="k-notice info" role="status">${STALE_KNOWLEDGE}.</p>`, review,
41
+ manage ? form('apply', acts(btn('Apply saved knowledge', { variant: 'primary', type: 'submit' }))) : '',
42
+ ]) })]);
23
43
  }
24
- const instructions = html `<pre class="knowledge-instructions">${view.knowledge.instructions || 'No project instructions yet.'}</pre>`;
25
- const editor = manage ? html `<details class="knowledge-editor"${draft?.instructions !== undefined ? html ` open` : ''}><summary>${view.knowledge.instructions ? 'Edit instructions' : 'Add instructions'}</summary>${form('instructions', html `<textarea aria-label="Project instructions" name="instructions" rows="5" maxlength="4000" placeholder="Goals, preferences, and what good work looks like…">${draft?.instructions ?? view.knowledge.instructions}</textarea><button>Save instructions</button>`)}</details>` : '';
26
- return html `<section class="knowledge knowledge-library">${error ? html `<p class="problem" role="alert">${error}</p>` : ''}${framedSection('Instructions', html `${instructions}${editor}`, 'knowledge-intro')}${framedSection('References', html `${refs.length ? refs : html `<p class="meta">Add a spec, design guide, or useful project document.</p>`}${manage ? html `<details class="card"${draft?.title && !draft.id ? html ` open` : ''}><summary>Add reference</summary>${form('save', referenceFields(draft?.id ? {} : draft ?? {}))}</details>` : ''}`)}${history.length ? html `<details class="knowledge frame"><summary class="frame-head">Change history</summary><div class="frame-panel"><ol>${history}</ol></div></details>` : ''}</section>`;
44
+ const instructions = html `<pre class="k-scode">${view.knowledge.instructions || 'No project instructions yet.'}</pre>`;
45
+ const editor = manage ? disclose(view.knowledge.instructions ? 'Edit instructions' : 'Add instructions', form('instructions', joinHtml([
46
+ textarea({ id: 'knowledge-instructions', name: 'instructions', rows: 5, value: draft?.instructions ?? view.knowledge.instructions, attrs: { 'aria-label': 'Project instructions', maxlength: 4000, placeholder: 'Goals, preferences, and what good work looks like…' } }),
47
+ acts(btn('Save instructions', { sm: true, type: 'submit' })),
48
+ ])), draft?.instructions !== undefined) : '';
49
+ const add = manage ? disclose('Add reference', form('save', referenceFields(draft?.id ? {} : draft ?? {}, 'new')), Boolean(draft?.title && !draft.id)) : '';
50
+ return joinHtml([
51
+ problem,
52
+ settingsSection({ id: 'instructions', title: 'Instructions', lead: 'What every task in this project reads first.', body: joinHtml([instructions, editor]) }),
53
+ settingsSection({ id: 'references', title: 'References', body: joinHtml([refs.length ? joinHtml(refs) : note('Add a spec, design guide, or useful project document.'), add]) }),
54
+ history.length ? settingsSection({ id: 'knowledge-history', title: 'Change history', body: html `<div class="k-slinks">${history}</div>` }) : '',
55
+ ]);
27
56
  }
28
57
  export function knowledgeContextHtml(snapshot) {
29
58
  if (!snapshot)
@@ -35,19 +64,47 @@ export function decisionsHtml(repo, decisions, csrf, canManage, draft, error) {
35
64
  const manage = canManage && !!csrf;
36
65
  const base = { repo };
37
66
  const form = (action, body, extra = {}) => postForm('/settings/knowledge/decision', body, { hidden: fields({ ...base, action, ...extra }) });
38
- const rows = decisions.map(d => html `<details class="card decision" id="decision-${d.id}"><summary><strong>${d.claim}</strong> <span class="meta">#${d.id} · ${d.decidedBy} · ${d.decidedAt.slice(0, 10)}</span></summary><p>${d.why}</p><p class="meta">${d.sourceKind}${d.sourceRef ? ` · ${d.sourceRef}` : ''}${d.supersedes === null ? '' : ` · replaces #${d.supersedes}`}</p>${manage ? form('retire', html `<label>Why it no longer holds<input name="reason" maxlength="500" required></label><button type="submit">Retire</button>`, { decision: d.id }) : ''}</details>`);
39
- const add = manage ? html `<details class="knowledge-editor"${draft?.claim !== undefined ? html ` open` : ''}><summary>Record a decision</summary>${form('record', html `<label>The choice, in one sentence<input name="claim" maxlength="240" value="${draft?.claim}" required></label><label>Why<textarea name="why" rows="3" maxlength="2000" required>${draft?.why}</textarea></label><button type="submit">Record</button>`)}</details>` : '';
40
- return framedSection('Decisions', html `${error ? html `<p class="problem" role="alert">${error}</p>` : ''}${rows.length ? rows : html `<p class="meta">No decisions yet. The lead suggests one when a conversation settles a choice.</p>`}${add}`);
67
+ const rows = decisions.map(d => settingsRecord({
68
+ title: d.claim, sub: d.why, when: html `<span class="k-num">#${d.id}</span> · ${d.decidedBy} · ${d.decidedAt.slice(0, 10)}`, attrs: { id: `decision-${d.id}` },
69
+ more: joinHtml([
70
+ note(`${d.sourceKind}${d.sourceRef ? ` · ${d.sourceRef}` : ''}${d.supersedes === null ? '' : ` · replaces #${d.supersedes}`}`),
71
+ manage ? disclose('Retire', form('retire', joinHtml([
72
+ formRow('Why it no longer holds', null, input({ id: `decision-${d.id}-reason`, name: 'reason', attrs: { maxlength: 500, required: true, 'aria-label': 'Why it no longer holds' } }), 'wide'),
73
+ acts(btn('Retire', { sm: true, variant: 'danger', type: 'submit' })),
74
+ ]), { decision: d.id })) : '',
75
+ ]),
76
+ }));
77
+ const add = manage ? disclose('Record a decision', form('record', joinHtml([
78
+ formRow('The choice, in one sentence', null, input({ id: 'decision-claim', name: 'claim', value: draft?.claim ?? '', attrs: { maxlength: 240, required: true, 'aria-label': 'The choice, in one sentence' } }), 'wide'),
79
+ formRow('Why', null, textarea({ id: 'decision-why', name: 'why', rows: 3, value: draft?.why ?? '', attrs: { maxlength: 2000, required: true, 'aria-label': 'Why' } }), 'top'),
80
+ acts(btn('Record', { sm: true, type: 'submit' })),
81
+ ])), draft?.claim !== undefined) : '';
82
+ return settingsSection({ id: 'decisions', title: 'Decisions', body: joinHtml([error ? notice(error) : '', rows.length ? joinHtml(rows) : note('No decisions yet. The lead suggests one when a conversation settles a choice.'), add]) });
41
83
  }
42
84
  export function memorySearchHtml(repo, query, hits) {
43
- const kind = (h) => h.kind === 'decision' ? html `<a href="#decision-${h.ref}">decision #${h.ref}</a>` : h.kind === 'conversation' ? 'conversation' : h.kind;
44
- const results = hits === null ? '' : hits.length === 0 ? html `<p class="meta">Nothing in project memory matches.</p>` : html `<ul class="memory-hits">${hits.map(h => html `<li><strong>${kind(h)}</strong> · ${h.title}<br><span class="meta">${h.snippet}</span></li>`)}</ul>`;
45
- return framedSection('Search project memory', html `<form class="field-row" method="get" action="/settings/knowledge"><input type="hidden" name="repo" value="${repo}"><label><span class="visually-hidden">Search project memory</span><input name="memory" value="${query}" maxlength="300" placeholder="Search decisions, references, lessons…" type="search" autocomplete="off"></label><button type="submit">Search</button></form>${results}`, 'memory-search');
85
+ const kind = (h) => h.kind === 'decision' ? html `<a class="k-link" href="#decision-${h.ref}">Decision #${h.ref}</a>` : h.kind === 'conversation' ? 'Conversation' : h.kind.charAt(0).toUpperCase() + h.kind.slice(1);
86
+ const results = hits === null ? '' : hits.length === 0 ? note('Nothing in project memory matches.') : joinHtml(hits.map(h => settingsRecord({ title: html `${kind(h)} · ${h.title}`, sub: h.snippet })));
87
+ return settingsSection({ id: 'memory-search', title: 'Search project memory', body: joinHtml([
88
+ html `<form method="get" action="/settings/knowledge" class="k-acts k-picker" role="search"><input type="hidden" name="repo" value="${repo}">${input({ id: 'memory-query', name: 'memory', type: 'search', value: query, attrs: { maxlength: 300, placeholder: 'Search decisions, references, lessons…', autocomplete: 'off', 'aria-label': 'Search project memory' } })}${btn('Search', { sm: true, type: 'submit' })}</form>`,
89
+ results,
90
+ ]) });
46
91
  }
47
92
  export function proposalsHtml(repo, proposals, status, csrf, canManage) {
48
93
  const manage = canManage && !!csrf;
49
- const line = status === null ? '' : html `<p class="meta memory-stats"><span>${status.sessions.analyzed}/${status.sessions.total} sessions read${status.sessions.failed ? ` · ${status.sessions.failed} failed` : ''}</span><span>${status.openGaps} open gap${status.openGaps === 1 ? '' : 's'}</span><span>Instructions ${status.instructionBytes}/${status.budgetBytes} bytes</span></p><details class="settings-more"><summary>Read new sessions</summary><p class="meta">Run <code>toolroll memory propose --repo ${repo}</code> on this computer.</p></details>`;
50
- const decide = (p, decision, label) => postForm('/settings/knowledge/proposal', html `<button type="submit">${label}</button>`, { attrs: { class: 'inline' }, hidden: fields({ repo, proposal: p.id, decision }) });
51
- const cards = proposals.map(p => html `<details class="card proposal" open><summary><strong>${p.title}</strong> <span class="meta">#${p.id} · ${p.kind} · ${p.sessions} sessions</span></summary><p>${p.rationale}</p>${p.beforeText ? html `<pre class="knowledge-instructions">- ${p.beforeText}</pre>` : ''}${p.afterText ? html `<pre class="knowledge-instructions">+ ${p.afterText}</pre>` : ''}<ul class="memory-evidence">${p.evidence.map(ev => html `<li>“${ev.quote.slice(0, 200)}” <span class="meta">${ev.session.split(':')[0] ?? ''}</span></li>`)}</ul>${manage ? html `${decide(p, 'accept', 'Accept')} ${decide(p, 'reject', 'Reject')}` : ''}</details>`);
52
- return framedSection('Proposed from sessions', html `${line}${cards.length ? cards : html `<p class="meta">Nothing to review. Suggestions appear when the same mistake shows up in two sessions.</p>`}`);
94
+ const line = status === null ? '' : joinHtml([
95
+ note(`${status.sessions.analyzed}/${status.sessions.total} sessions read${status.sessions.failed ? ` · ${status.sessions.failed} failed` : ''} · ${status.openGaps} open gap${status.openGaps === 1 ? '' : 's'} · Instructions ${status.instructionBytes}/${status.budgetBytes} bytes`),
96
+ disclose('Read new sessions', joinHtml([note('Run this on this computer:'), codeLine(`toolroll memory propose --repo ${repo}`)])),
97
+ ]);
98
+ const decide = (p, decision, label) => postForm('/settings/knowledge/proposal', btn(label, { sm: true, variant: decision === 'accept' ? 'outline' : 'ghost', type: 'submit' }), { hidden: fields({ repo, proposal: p.id, decision }) });
99
+ const cards = proposals.map(p => settingsRecord({
100
+ title: p.title, sub: html `<span class="k-num">#${p.id}</span> · ${p.kind} · ${p.sessions} sessions`,
101
+ action: manage ? html `${decide(p, 'accept', 'Accept')}${decide(p, 'reject', 'Reject')}` : '',
102
+ more: joinHtml([
103
+ note(p.rationale),
104
+ p.beforeText ? html `<pre class="k-scode">- ${p.beforeText}</pre>` : '',
105
+ p.afterText ? html `<pre class="k-scode">+ ${p.afterText}</pre>` : '',
106
+ html `<ul class="k-note">${p.evidence.map(ev => html `<li>“${ev.quote.slice(0, 200)}” · ${ev.session.split(':')[0] ?? ''}</li>`)}</ul>`,
107
+ ]),
108
+ }));
109
+ return settingsSection({ id: 'proposals', title: 'Proposed from sessions', body: joinHtml([line, cards.length ? joinHtml(cards) : note('Nothing to review. Suggestions appear when the same mistake shows up in two sessions.')]) });
53
110
  }
@@ -22,4 +22,6 @@ export type LedgerChainView = {
22
22
  /** Today (UTC, YYYY-MM-DD): the audit export's default range ends here. */
23
23
  today: string;
24
24
  };
25
+ /** The audit log (`/ledger`): what people and tokens did, newest first, as quiet timed rows, with the filters, the CSV
26
+ * and the sealed export. A kit list page: the top bar, then rows between hairlines. */
25
27
  export declare function ledgerBody(rows: LedgerEntry[], projects: readonly string[], params: URLSearchParams, chain?: LedgerChainView): Html;
@@ -1,57 +1,60 @@
1
1
  import { projectName } from "./project.js";
2
2
  import { exactWhenHtml } from "./when-html.js";
3
- import { html, postForm, styleElement } from "./html.js";
3
+ import { html, postForm } from "./html.js";
4
+ import { acts, btn, disclose, empty, infoLine, note, notice, pageHeader, select } from "./server/kit.js";
4
5
  /** The ledger's kinds of event, as the filter names them. */
5
6
  const EVENT_KINDS = [["work", "Work"], ["request", "Requests"], ["access", "Access"], ["sign-in", "Sign-ins"], ["policy", "Policy changes"]];
6
7
  function chainHtml(chain) {
7
8
  const status = chain.ok
8
- ? html `<p class="ledger-chain ok" data-ledger-chain="ok"><strong>Chain verified</strong> · ${chain.entries} entries through #${chain.through ?? 0} · head <code>${chain.head.slice(0, 16)}…</code>${chain.checkedAt === null ? "" : ` · checked in full ${chain.checkedAt.slice(11, 16)} UTC`}</p>`
9
- : html `<p class="ledger-chain problem" role="alert" data-ledger-chain="broken"><strong>Chain broken</strong> · ${chain.problem?.what ?? "the chain doesn't verify"}</p>`;
10
- const latest = chain.latest === null ? "" : html `<p class="meta ledger-checkpoint">Latest checkpoint (${chain.latest.at.slice(0, 16).replace("T", " ")} UTC), copy it somewhere outside this machine: <code class="ledger-checkpoint-value">${chain.latest.through}:${chain.latest.hash}</code></p>`;
11
- const make = chain.csrf === null ? "" : postForm("/ledger/checkpoint", html `<button type="submit">Make a checkpoint</button>`, { attrs: { class: "ledger-checkpoint-form" } });
9
+ ? infoLine(html `<span data-ledger-chain="ok">Chain verified · ${chain.entries} entries through #${chain.through ?? 0} · head <span class="k-mono">${chain.head.slice(0, 16)}…</span>${chain.checkedAt === null ? "" : ` · checked in full ${chain.checkedAt.slice(11, 16)} UTC`}</span>`)
10
+ : notice(html `<span data-ledger-chain="broken">Chain broken: ${chain.problem?.what ?? "the chain doesn't verify"}</span>`);
11
+ const latest = chain.latest === null ? "" : note(html `Latest checkpoint (${chain.latest.at.slice(0, 16).replace("T", " ")} UTC). Copy it somewhere outside this machine: <span class="k-mono h-select">${chain.latest.through}:${chain.latest.hash}</span>`);
12
+ const make = chain.csrf === null ? "" : postForm("/ledger/checkpoint", btn("Make a checkpoint", { sm: true, type: "submit", attrs: { "data-k-busy": "Making…" } }), { attrs: { class: "h-inline" } });
12
13
  const start = new Date(Date.parse(`${chain.today}T00:00:00Z`) - 29 * 86_400_000).toISOString().slice(0, 10);
13
- const exportForm = html `<details class="ledger-filter-panel"><summary>Audit export</summary><form method="get" action="/ledger/export" class="ledger-filters ledger-export" aria-label="Audit export"><label>From<input type="date" name="from" value="${start}" required></label><label>To<input type="date" name="to" value="${chain.today}" required></label><button type="submit">Download</button></form><p class="meta">Every entry in the range with its seal, and an evidence pack for each task it names. JSON.</p></details>`;
14
- return html `<section class="ledger-chain-panel" aria-label="Ledger integrity">${status}${latest}${make}${exportForm}</section>`;
14
+ const exportForm = disclose("Audit export", html `<form method="get" action="/ledger/export" class="h-filters h-export" aria-label="Audit export">\
15
+ <div class="k-field"><label class="k-label" for="ledger-from">From</label><input class="k-input" id="ledger-from" type="date" name="from" value="${start}" required></div>\
16
+ <div class="k-field"><label class="k-label" for="ledger-to">To</label><input class="k-input" id="ledger-to" type="date" name="to" value="${chain.today}" required></div>\
17
+ <div class="k-acts">${btn("Download", { sm: true, type: "submit" })}</div></form>${note("Every entry in the range with its seal, and a full record for each task it names. JSON.")}`);
18
+ return html `<section class="h-chain" aria-label="Audit log integrity">${status}${latest}${make}${exportForm}</section>`;
15
19
  }
16
- const LEDGER_CSS = `
17
- .ledger-heading{display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap}.ledger-heading h1{margin:0}.ledger-caption{margin:8px 0 12px}
18
- .ledger-filter-panel{margin-bottom:12px}.ledger-filter-panel summary{font-size:13px;padding:6px 0;cursor:pointer}.ledger-filters{display:grid;grid-template-columns:repeat(5,minmax(0,1fr)) auto auto;gap:8px;align-items:end;margin:0 0 12px}.ledger-filters label{display:grid;gap:3px;min-width:0;font-size:12px}.ledger-filters input,.ledger-filters select{width:100%;min-width:0;box-sizing:border-box;margin:0;padding:6px 8px;font-size:13px}.ledger-filters button{padding:7px 12px}.ledger-filters>a{padding:8px 0;font-size:13px}
19
- .ledger-detail{display:block;color:var(--muted-foreground);font-size:12px}.ledger-list{border:1px solid var(--border);border-radius:8px;overflow:hidden}.ledger-event{display:grid;grid-template-columns:140px minmax(90px,1fr) minmax(160px,2fr) minmax(100px,1fr) minmax(100px,1fr);gap:8px;align-items:baseline;padding:7px 10px;border-bottom:1px solid var(--border);font-size:13px;line-height:1.4;overflow-wrap:anywhere}.ledger-event:last-child{border-bottom:0}.ledger-event .actor{color:inherit;font-weight:600;text-decoration:none}.ledger-time,.ledger-place,.ledger-result small{font-size:11px;color:var(--muted-foreground)}.ledger-time time{font-variant-numeric:tabular-nums}.ledger-place a{display:block}.ledger-result small{display:block}.ledger-labels{font-size:11px;color:var(--muted-foreground);font-weight:600}.ledger-empty{padding:12px}
20
- @media(max-width:1000px){.ledger-filters{grid-template-columns:repeat(3,minmax(0,1fr)) auto}.ledger-event{grid-template-columns:122px minmax(80px,1fr) minmax(130px,2fr) minmax(85px,1fr)}.ledger-place{grid-column:2/4}.ledger-result{grid-column:4;grid-row:1/3}.ledger-labels{display:none}}
21
- @media(max-width:600px){.ledger-filters{grid-template-columns:repeat(2,minmax(0,1fr))}.ledger-event{grid-template-columns:minmax(0,1fr) auto;gap:2px 8px;padding:8px 10px}.ledger-actor{grid-column:1;grid-row:1}.ledger-action{grid-column:1;grid-row:2}.ledger-result{grid-column:2;grid-row:1/3;text-align:right}.ledger-time{grid-column:1;grid-row:3}.ledger-place{grid-column:2;grid-row:3;text-align:right}.ledger-place a{display:inline}.ledger-heading>a{font-size:13px}}
22
- .ledger-chain-panel{margin:0 0 12px}.ledger-chain{margin:0 0 4px;font-size:13px}.ledger-chain.ok strong{color:var(--success)}.ledger-checkpoint code{overflow-wrap:anywhere;user-select:all}.ledger-checkpoint-form{margin:6px 0 0}.ledger-export{grid-template-columns:repeat(2,minmax(0,180px)) auto;margin-bottom:4px}
23
- `;
20
+ /** The audit log (`/ledger`): what people and tokens did, newest first, as quiet timed rows, with the filters, the CSV
21
+ * and the sealed export. A kit list page: the top bar, then rows between hairlines. */
24
22
  export function ledgerBody(rows, projects, params, chain) {
25
- const selected = (key, value) => params.get(key) === value ? html ` selected` : "";
26
23
  const more = rows.length > 50;
27
24
  const shown = rows.slice(0, 50);
28
- const fresh = new URLSearchParams();
25
+ const kept = new URLSearchParams();
29
26
  for (const key of ["project", "actor", "source", "outcome", "task"]) {
30
27
  const value = params.get(key);
31
28
  if (value)
32
- fresh.set(key, value);
29
+ kept.set(key, value);
33
30
  }
34
- const next = new URLSearchParams(fresh);
31
+ const next = new URLSearchParams(kept);
35
32
  if (shown.length)
36
33
  next.set("before", String(shown.at(-1).id));
37
- const csv = new URLSearchParams(fresh);
34
+ const csv = new URLSearchParams(kept);
38
35
  csv.set("format", "csv");
39
- return html `${styleElement(LEDGER_CSS)}<div class="ledger-heading"><h1>Action ledger</h1><a href="/ledger?${csv.toString()}" download>Export CSV</a></div>${chain === undefined ? "" : chainHtml(chain)}
40
- <p class="meta ledger-caption">${shown.length} actions on this page · CSV includes all matching actions · Times in UTC</p>
41
- <details class="ledger-filter-panel"><summary>Filters · ${fresh.size === 0 ? "all actions" : `${fresh.size} active`}</summary>
42
- <form method="get" action="/ledger" class="ledger-filters" aria-label="Filter action ledger">
43
- <label>Project<select name="project"><option value="">All accessible projects</option>${projects.map(repo => html `<option value="${repo}"${selected("project", repo)}>${projectName(repo)}</option>`)}</select></label>
44
- <label>Person or worker<input type="text" name="actor" value="${params.get("actor") ?? ""}" placeholder="Any actor"></label>
45
- <label>Events<select name="source"><option value="">All events</option>${EVENT_KINDS.map(([value, label]) => html `<option value="${value}"${selected("source", value)}>${label}</option>`)}</select></label>
46
- <label>Outcome<input type="text" name="outcome" value="${params.get("outcome") ?? ""}" placeholder="Any outcome"></label>
47
- <label>Task<input type="text" name="task" value="${params.get("task") ?? ""}" placeholder="Any task"></label>
48
- <button type="submit">Filter</button><a href="/ledger">Clear</a></form></details>
49
- <div class="ledger-list" aria-label="Action history"><div class="ledger-event ledger-labels" aria-hidden="true"><span>Time</span><span>Actor</span><span>Action</span><span>Project / work</span><span>Outcome / event</span></div>${shown.length === 0 ? html `<p class="ledger-empty">No actions match these filters yet.</p>` : shown.map(row => {
50
- const subject = row.runId !== null ? html `<a href="/r/${row.runId}">Run #${row.runId}</a>` : row.taskId !== null ? html `<a href="/t/${encodeURIComponent(row.taskId)}">${row.taskId}</a>` : "";
51
- const byActor = new URLSearchParams(fresh);
52
- byActor.set("actor", row.actor);
53
- return html `<article class="ledger-event" data-ledger-id="${row.id}"><div class="ledger-time">${exactWhenHtml(row.at)}</div><div class="ledger-actor"><a class="actor" href="/ledger?${byActor.toString()}" title="Filter by this actor">${row.actor}</a></div><div class="ledger-action">${row.action}${row.detail === null ? "" : html `<small class="ledger-detail">${row.detail}</small>`}</div><div class="ledger-place">${row.repo === null ? "Instance" : projectName(row.repo)} ${subject}</div><div class="ledger-result">${row.outcome}<small>${row.source}</small></div></article>`;
54
- })}</div>
55
- <p class="row"><a href="/ledger?${fresh.toString()}">Newest actions</a>${more ? html `<a rel="next" href="/ledger?${next.toString()}">Older actions</a>` : ""}</p>
56
- <p class="meta">Accepted requests record acceptance; worker events record results. History starts from this upgrade. Review evidence is on each task or run.</p>`;
36
+ const text = (id, label, name, placeholder) => html `<div class="k-field"><label class="k-label" for="${id}">${label}</label><input class="k-input" id="${id}" type="text" name="${name}" value="${params.get(name) ?? ""}" placeholder="${placeholder}"></div>`;
37
+ const filters = disclose(`Filters · ${kept.size === 0 ? "all actions" : `${kept.size} active`}`, html `<form method="get" action="/ledger" class="h-filters" aria-label="Filter the audit log">\
38
+ <div class="k-field"><label class="k-label" for="ledger-project">Project</label>${select({ id: "ledger-project", name: "project", wide: true, current: params.get("project") ?? "", items: [["", "All your projects"], ...projects.map(repo => [repo, projectName(repo)])] })}</div>\
39
+ ${text("ledger-actor", "Person or worker", "actor", "Anyone")}\
40
+ <div class="k-field"><label class="k-label" for="ledger-source">Events</label>${select({ id: "ledger-source", name: "source", wide: true, current: params.get("source") ?? "", items: [["", "All events"], ...EVENT_KINDS] })}</div>\
41
+ ${text("ledger-outcome", "Outcome", "outcome", "Any outcome")}${text("ledger-task", "Task", "task", "Any task")}\
42
+ <div class="k-acts">${btn("Filter", { sm: true, type: "submit" })}${btn("Clear", { sm: true, variant: "ghost", href: "/ledger" })}</div></form>`, kept.size > 0);
43
+ const list = shown.length === 0
44
+ ? empty({ title: kept.size === 0 ? "Nothing yet." : "No actions match these filters.", text: kept.size === 0 ? "Approvals, invites, tokens and sign-ins appear here." : "Clear the filters to see every action." })
45
+ : html `<div class="h-audit-list" aria-label="Action history">${shown.map(row => {
46
+ const subject = row.runId !== null ? html `<a class="k-link" href="/r/${row.runId}">Run #${row.runId}</a>` : row.taskId !== null ? html `<a class="k-link k-mono" href="/t/${encodeURIComponent(row.taskId)}">${row.taskId}</a>` : "";
47
+ const byActor = new URLSearchParams(kept);
48
+ byActor.set("actor", row.actor);
49
+ return html `<article class="h-audit" data-ledger-id="${row.id}"><span class="h-audit-time k-num">${exactWhenHtml(row.at)}</span>\
50
+ <span class="h-audit-what"><a class="h-actor" href="/ledger?${byActor.toString()}" title="Only this actor's actions">${row.actor}</a> ${row.action}${row.detail === null ? "" : html `<span class="h-audit-detail">${row.detail}</span>`}</span>\
51
+ <span class="h-audit-place">${row.repo === null ? "Instance" : projectName(row.repo)} ${subject}</span><span class="h-audit-out">${row.outcome} · ${row.source}</span></article>`;
52
+ })}</div>`;
53
+ const paging = kept.has("before") || params.has("before") || more
54
+ ? acts(params.has("before") ? btn("Newest actions", { sm: true, variant: "ghost", href: `/ledger?${kept.toString()}` }) : "", more ? btn("Older actions", { sm: true, variant: "ghost", href: `/ledger?${next.toString()}`, attrs: { rel: "next" } }) : "")
55
+ : "";
56
+ return html `<div class="h-page h-ledger" data-kit-page>${pageHeader("Audit log", {
57
+ ctx: "What people and tokens did, newest first. Times in UTC.",
58
+ right: btn("Export CSV", { sm: true, variant: "ghost", href: `/ledger?${csv.toString()}`, attrs: { download: true } }),
59
+ })}<div class="h-tools-row">${chain === undefined ? "" : chainHtml(chain)}${filters}</div>${list}<div class="h-foot-row">${paging}${note("Accepted requests record acceptance; worker events record results. History starts from the upgrade that added it. Review evidence is on each task or run. The CSV holds every matching action.")}</div></div>`;
57
60
  }
@@ -1,8 +1,11 @@
1
- /** Settings → Models: the CLIs on this computer, the default agent for each
2
- * role picked from live lists, and the models that just arrived. */
1
+ /** Settings → Models, in the kit's Settings frame (a sub-page of "Agents and models"): what is in use and the one
2
+ * choice (a preset, previewed before it applies), then the model for each role behind a disclosure, the CLIs on
3
+ * this computer and the models that just arrived. Saving is an instance operator's step-up. */
3
4
  import { type Html } from "./html.js";
5
+ import { type ApprovalFacts } from "./design/kit.js";
4
6
  import type { ModelOption, RuntimeState, SeenModel, WatchState } from "./model-catalog.js";
5
- export declare const MODELS_CSS = ".models{max-width:780px;min-width:0;overflow-wrap:anywhere}.models .card{padding:16px 20px;margin:12px 0;min-width:0}.models h2{margin:24px 0 8px}.models form{margin:0}.models label{display:grid;gap:6px}.models select,.models input[type=search]{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.models button,.models summary,.models .button-link{min-height:44px}.models button{white-space:nowrap}.models summary{padding:12px 0;cursor:pointer}.models .tool{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}.models .role{display:grid;grid-template-columns:minmax(0,1fr);gap:8px}.models .role-row{display:flex;gap:12px;align-items:end;flex-wrap:wrap}.models .role-row select{flex:1 1 240px;width:auto;min-width:0}.models .role-name{font-weight:600}.models .new{font-weight:600}.models ul{padding-left:20px}.models .meta{font-size:.86rem}.models .role-read{display:grid;gap:2px;padding:8px 0}.models .editor{display:grid;gap:14px}.models .editor .role select{width:100%}.models .tiers{border-top:1px solid var(--so-line)}.models .tiers .role{margin:0 0 14px}.models .mono{font-family:var(--font-mono,ui-monospace,monospace);font-size:.84rem}.models .preset{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap}.models .preset p{margin:4px 0 0}.models .preset>div{flex:1 1 280px;min-width:0}.models .button-link{display:inline-flex;align-items:center;padding:0 14px;border:1px solid var(--so-input-line);border-radius:8px;text-decoration:none;color:var(--so-ink);font-weight:600}.models table{width:100%;border-collapse:collapse}.models th,.models td{text-align:left;vertical-align:top;padding:8px 6px;border-bottom:1px solid var(--so-line)}.models .diff th{width:38%;font-weight:600}.models .diff ul{margin:4px 0 0}.models .preview{border-color:var(--so-input-line)}.models .preview form{display:grid;gap:10px;margin-top:12px}.models .preview .cancel{justify-self:start;display:inline-flex;align-items:center;min-height:44px}.models .sizing dt{font-weight:600;margin-top:8px}.models .sizing dd{margin:2px 0 0}.models .editor label:has(input[type=password]){margin-top:4px}@media(max-width:600px){.models .card{padding:14px}.models .diff th,.models .diff td{display:block;width:auto;border:0;padding:4px 0}.models .diff tr{display:block;border-bottom:1px solid var(--so-line);padding:6px 0}.models .recent td{display:block;border:0;padding:2px 0}.models .recent tr{display:block;border-bottom:1px solid var(--so-line);padding:6px 0}.models .recent thead{display:none}}";
7
+ /** The page's own layout on the kit: one-column preset cards, the role pickers in the form rows' control column. */
8
+ export declare const MODELS_CSS: string;
6
9
  /** One row of the editor: a role at the scope being edited. */
7
10
  export type RoleView = {
8
11
  /** The slot's form field (model-choices.ts slotKey). */
@@ -93,6 +96,8 @@ export type ModelsView = {
93
96
  taskId: string;
94
97
  title: string;
95
98
  }[];
99
+ /** What this request's approval shows (approvalFactsNow): a step-up asks for the password unless Passwordless is on. */
100
+ facts?: ApprovalFacts;
96
101
  };
97
102
  export declare function modelsHtml(view: ModelsView): Html;
98
103
  /** Filters one long picker as you type; the saved choice always stays listed. */