toolroll 0.9.54 → 0.9.55

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 (199) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +5 -4
  3. package/dist/agent-onboard.js +4 -1
  4. package/dist/approval-rules-ui.d.ts +3 -2
  5. package/dist/approval-rules-ui.js +15 -20
  6. package/dist/assignment-ui.d.ts +3 -2
  7. package/dist/assignment-ui.js +4 -11
  8. package/dist/backup-ui.d.ts +2 -1
  9. package/dist/backup-ui.js +16 -27
  10. package/dist/brand-mark.d.ts +3 -2
  11. package/dist/brand-mark.js +4 -4
  12. package/dist/browser/workspace.css +1 -1
  13. package/dist/browser/workspace.js +25 -25
  14. package/dist/browser-shell.d.ts +2 -1
  15. package/dist/browser-shell.js +9 -9
  16. package/dist/browser-workspace.d.ts +2 -3
  17. package/dist/browser-workspace.js +0 -3
  18. package/dist/chat-polish.d.ts +5 -4
  19. package/dist/chat-polish.js +9 -10
  20. package/dist/check-levels-ui.d.ts +2 -1
  21. package/dist/check-levels-ui.js +26 -53
  22. package/dist/cli.d.ts +3 -1
  23. package/dist/cli.js +9 -3
  24. package/dist/coding-shipping-ui.d.ts +2 -1
  25. package/dist/coding-shipping-ui.js +6 -6
  26. package/dist/coding-ui.d.ts +3 -1
  27. package/dist/coding-ui.js +30 -29
  28. package/dist/contracts/cli.d.ts +1 -3
  29. package/dist/contracts/console-api.d.ts +1 -1
  30. package/dist/contracts/console-api.js +6 -5
  31. package/dist/contracts/health.d.ts +1 -3
  32. package/dist/control-ui.d.ts +5 -4
  33. package/dist/control-ui.js +9 -34
  34. package/dist/credentials-ui.d.ts +5 -4
  35. package/dist/credentials-ui.js +23 -24
  36. package/dist/demo-chat.d.ts +3 -3
  37. package/dist/demo-chat.js +29 -76
  38. package/dist/discord-settings.d.ts +3 -2
  39. package/dist/discord-settings.js +17 -42
  40. package/dist/envelope.js +10 -0
  41. package/dist/evidence-pack.d.ts +2 -1
  42. package/dist/evidence-pack.js +45 -49
  43. package/dist/export-ui.d.ts +7 -2
  44. package/dist/export-ui.js +4 -10
  45. package/dist/flow-gallery-ui.d.ts +4 -3
  46. package/dist/flow-gallery-ui.js +28 -43
  47. package/dist/flow-starters-ui.d.ts +2 -1
  48. package/dist/flow-starters-ui.js +12 -15
  49. package/dist/flow-triggers.d.ts +1 -0
  50. package/dist/flow-triggers.js +16 -14
  51. package/dist/flows-ui.d.ts +4 -3
  52. package/dist/flows-ui.js +19 -30
  53. package/dist/flows.js +2 -1
  54. package/dist/guides.js +108 -3
  55. package/dist/html.d.ts +76 -0
  56. package/dist/html.js +173 -0
  57. package/dist/integrations-ui.d.ts +2 -1
  58. package/dist/integrations-ui.js +19 -23
  59. package/dist/kits-ui.d.ts +5 -4
  60. package/dist/kits-ui.js +18 -20
  61. package/dist/knowledge-ui.d.ts +6 -5
  62. package/dist/knowledge-ui.js +26 -25
  63. package/dist/lead-context.d.ts +11 -1
  64. package/dist/lead-context.js +6 -8
  65. package/dist/ledger-view.d.ts +2 -1
  66. package/dist/ledger-view.js +27 -29
  67. package/dist/limits-ui.d.ts +2 -1
  68. package/dist/limits-ui.js +7 -7
  69. package/dist/live-bus.d.ts +50 -8
  70. package/dist/live-bus.js +133 -50
  71. package/dist/mcp-oauth.js +35 -36
  72. package/dist/models-ui.d.ts +2 -1
  73. package/dist/models-ui.js +25 -35
  74. package/dist/monitoring-ui.d.ts +4 -3
  75. package/dist/monitoring-ui.js +13 -26
  76. package/dist/openrouter-models.d.ts +2 -1
  77. package/dist/openrouter-models.js +5 -9
  78. package/dist/operate.js +11 -7
  79. package/dist/people-audit-ui.d.ts +2 -1
  80. package/dist/people-audit-ui.js +13 -13
  81. package/dist/policy-ui.d.ts +3 -2
  82. package/dist/policy-ui.js +17 -23
  83. package/dist/project-delete-ui.d.ts +4 -3
  84. package/dist/project-delete-ui.js +15 -27
  85. package/dist/pull-request-ui.d.ts +2 -1
  86. package/dist/pull-request-ui.js +17 -32
  87. package/dist/recipe-ui.d.ts +8 -7
  88. package/dist/recipe-ui.js +39 -32
  89. package/dist/release-adapters.d.ts +60 -0
  90. package/dist/release-adapters.js +435 -0
  91. package/dist/release-cli.d.ts +24 -0
  92. package/dist/release-cli.js +181 -0
  93. package/dist/release-gate.d.ts +80 -0
  94. package/dist/release-gate.js +150 -0
  95. package/dist/release-links.d.ts +29 -0
  96. package/dist/release-links.js +102 -0
  97. package/dist/release.d.ts +331 -0
  98. package/dist/release.js +550 -0
  99. package/dist/reply-shape.d.ts +2 -1
  100. package/dist/reply-shape.js +5 -5
  101. package/dist/repository-context-ui.d.ts +2 -1
  102. package/dist/repository-context-ui.js +2 -7
  103. package/dist/request-budget-ui.d.ts +2 -1
  104. package/dist/request-budget-ui.js +16 -16
  105. package/dist/result-review.d.ts +2 -3
  106. package/dist/result-review.js +3 -9
  107. package/dist/retention-ui.d.ts +7 -1
  108. package/dist/retention-ui.js +7 -11
  109. package/dist/serve.d.ts +11 -2
  110. package/dist/serve.js +69 -58
  111. package/dist/server/chat.d.ts +3 -6
  112. package/dist/server/chat.js +969 -1006
  113. package/dist/server/chrome.d.ts +291 -0
  114. package/dist/server/chrome.js +747 -0
  115. package/dist/server/console.css +2819 -0
  116. package/dist/server/flows.d.ts +2 -6
  117. package/dist/server/flows.js +840 -766
  118. package/dist/server/guards.d.ts +1 -1
  119. package/dist/server/guards.js +5 -2
  120. package/dist/server/handler-context.d.ts +1 -1
  121. package/dist/server/handler-context.js +1 -1
  122. package/dist/server/handler-registry.d.ts +8 -6
  123. package/dist/server/handler-registry.js +14 -2
  124. package/dist/server/http.d.ts +274 -0
  125. package/dist/server/http.js +239 -0
  126. package/dist/server/live.d.ts +34 -0
  127. package/dist/server/live.js +309 -0
  128. package/dist/server/pages.d.ts +2 -6
  129. package/dist/server/pages.js +808 -789
  130. package/dist/server/people-tokens.d.ts +1 -6
  131. package/dist/server/people-tokens.js +580 -572
  132. package/dist/server/remote.d.ts +4 -7
  133. package/dist/server/remote.js +37 -70
  134. package/dist/server/render-chat.d.ts +402 -0
  135. package/dist/server/render-chat.js +927 -0
  136. package/dist/server/render-pages.d.ts +535 -0
  137. package/dist/server/render-pages.js +1277 -0
  138. package/dist/server/render-people.d.ts +32 -0
  139. package/dist/server/render-people.js +121 -0
  140. package/dist/server/render-results.d.ts +744 -0
  141. package/dist/server/render-results.js +2237 -0
  142. package/dist/server/render-settings.d.ts +147 -0
  143. package/dist/server/render-settings.js +577 -0
  144. package/dist/server/render-tasks.d.ts +632 -0
  145. package/dist/server/render-tasks.js +2345 -0
  146. package/dist/server/request-context.d.ts +46 -0
  147. package/dist/server/request-context.js +17 -0
  148. package/dist/server/route-table.d.ts +25 -3
  149. package/dist/server/route-table.js +163 -75
  150. package/dist/server/runtime.d.ts +15 -7
  151. package/dist/server/runtime.js +7 -1
  152. package/dist/server/session.d.ts +138 -0
  153. package/dist/server/session.js +94 -0
  154. package/dist/server/settings.d.ts +1 -5
  155. package/dist/server/settings.js +2181 -2117
  156. package/dist/server/task-views.d.ts +11 -4
  157. package/dist/server/task-views.js +26 -22
  158. package/dist/server/tasks.d.ts +9 -9
  159. package/dist/server/tasks.js +1486 -1418
  160. package/dist/server-telemetry.d.ts +4 -3
  161. package/dist/server-telemetry.js +4 -8
  162. package/dist/skills-ui.d.ts +4 -3
  163. package/dist/skills-ui.js +14 -17
  164. package/dist/skills.d.ts +1 -1
  165. package/dist/skills.js +2 -1
  166. package/dist/slack-settings.d.ts +3 -2
  167. package/dist/slack-settings.js +17 -42
  168. package/dist/spend-ui.d.ts +2 -1
  169. package/dist/spend-ui.js +9 -32
  170. package/dist/sso-ui.d.ts +3 -2
  171. package/dist/sso-ui.js +26 -27
  172. package/dist/storage-ui.d.ts +2 -1
  173. package/dist/storage-ui.js +14 -33
  174. package/dist/surface.js +14 -3
  175. package/dist/task-composer.d.ts +2 -1
  176. package/dist/task-composer.js +7 -7
  177. package/dist/task-status.d.ts +4 -3
  178. package/dist/task-status.js +15 -16
  179. package/dist/team-http.d.ts +5 -9
  180. package/dist/team-http.js +5 -63
  181. package/dist/team-ui.d.ts +2 -1
  182. package/dist/team-ui.js +2 -5
  183. package/dist/teammates-ui.d.ts +5 -4
  184. package/dist/teammates-ui.js +61 -90
  185. package/dist/teams-settings.d.ts +3 -2
  186. package/dist/teams-settings.js +20 -31
  187. package/dist/telegram-settings.d.ts +3 -2
  188. package/dist/telegram-settings.js +14 -25
  189. package/dist/toolroll-update-ui.d.ts +3 -2
  190. package/dist/toolroll-update-ui.js +35 -39
  191. package/dist/tools-ui.d.ts +5 -2
  192. package/dist/tools-ui.js +31 -31
  193. package/dist/when-html.d.ts +11 -3
  194. package/dist/when-html.js +5 -6
  195. package/dist/workspace-ui.d.ts +4 -2
  196. package/dist/workspace-ui.js +10 -10
  197. package/package.json +1 -1
  198. package/dist/server/shared.d.ts +0 -3151
  199. package/dist/server/shared.js +0 -12001
package/dist/html.d.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Server HTML, escaped by construction.
3
+ *
4
+ * - `Html` is markup this module made. Nothing else can make one: the class
5
+ * is private and branded, so a string never passes for markup.
6
+ * - `html` is the only way to write markup. Its literal text is the
7
+ * author's; every interpolated value is text and is escaped, unless it is
8
+ * already `Html`. Arrays render in order; null, undefined and booleans
9
+ * render nothing, so `${cond && html`…`}` reads naturally.
10
+ * - `postForm` is the only way to write a POST form. It adds the request's
11
+ * CSRF field itself, so a form cannot forget it.
12
+ * - Page sinks (the shell, `respond` for text/html, the workspace's page
13
+ * snapshot) take `Html` and turn it into bytes with `htmlString`.
14
+ *
15
+ * The script, style and JSON constructors exist for the page shell: their
16
+ * sources are code, and each refuses content that could close its element.
17
+ *
18
+ * This module has no server dependencies (the browser bundle shares time
19
+ * helpers that use it); the server's request context supplies the CSRF token.
20
+ */
21
+ declare const MADE_HERE: unique symbol;
22
+ /** Markup this module made. Only this module holds the token its constructor asks for. */
23
+ export declare class Html {
24
+ #private;
25
+ constructor(token: typeof MADE_HERE, markup: string);
26
+ /** The bytes, for a sink. */
27
+ static markupOf(fragment: Html): string;
28
+ toString(): string;
29
+ toJSON(): string;
30
+ }
31
+ export type HtmlValue = Html | string | number | bigint | boolean | null | undefined | readonly HtmlValue[];
32
+ export declare const isHtml: (value: unknown) => value is Html;
33
+ /** The markup of an `Html` value, for a sink that writes bytes. */
34
+ export declare function htmlString(fragment: Html): string;
35
+ /** Markup with every interpolation escaped as text, except values that are already `Html`. */
36
+ export declare function html(strings: TemplateStringsArray, ...values: readonly HtmlValue[]): Html;
37
+ /** Parts in order, with an optional separator between them. */
38
+ export declare function joinHtml(parts: Iterable<HtmlValue>, separator?: HtmlValue): Html;
39
+ /** Plain text, escaped (for a sink that takes Html when the content is only words). */
40
+ export declare const textHtml: (text: string) => Html;
41
+ export type AttributeValue = string | number | boolean | null | undefined;
42
+ /** Attributes as `name="value"` pairs: true is a bare attribute; false, null and undefined are left out. */
43
+ export declare function attributes(values: Readonly<Record<string, AttributeValue>>): Html;
44
+ export type PostFormOptions = {
45
+ /** Attributes on the form element (class, id, aria-label, data-…). */
46
+ attrs?: Readonly<Record<string, AttributeValue>>;
47
+ /** Hidden fields, after the CSRF field (null and undefined are left out). */
48
+ hidden?: Readonly<Record<string, string | number | null | undefined>>;
49
+ /** The open project's revision this page was rendered under: a stale tab's post is refused. */
50
+ projectRevision?: number | string | null;
51
+ /** Where the action returns afterwards (a same-site path). */
52
+ returnTo?: string | null;
53
+ /** Forms before a session exists (sign-in, sign-up, a join link) have no CSRF field. */
54
+ signedOut?: true;
55
+ };
56
+ /** The server's request context supplies the request's CSRF token (server/request-context.ts). */
57
+ export declare function provideFormToken(source: () => string | undefined): void;
58
+ /**
59
+ * A POST form: the action, the request's one CSRF field, the optional
60
+ * project revision and return fields, other hidden fields, then the body.
61
+ * Outside a console request there is no CSRF to give, so it refuses.
62
+ */
63
+ export declare function postForm(action: string, body: HtmlValue, options?: PostFormOptions): Html;
64
+ /**
65
+ * Markup edited after rendering: each match of `pattern` in the page is replaced by markup the replacer builds
66
+ * (with html`…`), or kept as it was (null), so nothing unescaped can enter. For whole-page passes (a sign-in that
67
+ * stands in for passwords, the browser shell's insertions).
68
+ */
69
+ export declare function replaceMarkup(page: Html, pattern: RegExp, replace: (match: string, ...groups: string[]) => Html | null): Html;
70
+ /** An inline script whose source is code. Its text may not close the element or open a comment. */
71
+ export declare function scriptElement(source: string, attrs?: Readonly<Record<string, AttributeValue>>): Html;
72
+ /** A style element whose rules are code. */
73
+ export declare function styleElement(css: string, attrs?: Readonly<Record<string, AttributeValue>>): Html;
74
+ /** Inert JSON for a page script to read: no character of it can end the element. */
75
+ export declare function jsonScript(value: unknown, attrs?: Readonly<Record<string, AttributeValue>>): Html;
76
+ export {};
package/dist/html.js ADDED
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Server HTML, escaped by construction.
3
+ *
4
+ * - `Html` is markup this module made. Nothing else can make one: the class
5
+ * is private and branded, so a string never passes for markup.
6
+ * - `html` is the only way to write markup. Its literal text is the
7
+ * author's; every interpolated value is text and is escaped, unless it is
8
+ * already `Html`. Arrays render in order; null, undefined and booleans
9
+ * render nothing, so `${cond && html`…`}` reads naturally.
10
+ * - `postForm` is the only way to write a POST form. It adds the request's
11
+ * CSRF field itself, so a form cannot forget it.
12
+ * - Page sinks (the shell, `respond` for text/html, the workspace's page
13
+ * snapshot) take `Html` and turn it into bytes with `htmlString`.
14
+ *
15
+ * The script, style and JSON constructors exist for the page shell: their
16
+ * sources are code, and each refuses content that could close its element.
17
+ *
18
+ * This module has no server dependencies (the browser bundle shares time
19
+ * helpers that use it); the server's request context supplies the CSRF token.
20
+ */
21
+ const MADE_HERE = Symbol("html");
22
+ /** Markup this module made. Only this module holds the token its constructor asks for. */
23
+ export class Html {
24
+ #markup;
25
+ constructor(token, markup) {
26
+ if (token !== MADE_HERE)
27
+ throw new TypeError("Html is made by html`…`");
28
+ this.#markup = markup;
29
+ }
30
+ /** The bytes, for a sink. */
31
+ static markupOf(fragment) { return fragment.#markup; }
32
+ // Coercing markup to a string (`${fragment}` in a plain template, `+`, join) would lose the brand and
33
+ // be escaped again downstream. Under test that is an error; a running server keeps serving the markup.
34
+ toString() {
35
+ if (STRICT)
36
+ throw new Error("Html coerced to a string: interpolate it with html`…` or hand it to a sink");
37
+ return this.#markup;
38
+ }
39
+ toJSON() {
40
+ if (STRICT)
41
+ throw new Error("Html serialized as JSON: convert it with htmlString() first");
42
+ return this.#markup;
43
+ }
44
+ }
45
+ // Under test, a coercion is an error to fix; a running server keeps serving the markup.
46
+ const STRICT = typeof process !== "undefined" && process.env["VITEST"] !== undefined;
47
+ const make = (markup) => new Html(MADE_HERE, markup);
48
+ export const isHtml = (value) => value instanceof Html;
49
+ /** The markup of an `Html` value, for a sink that writes bytes. */
50
+ export function htmlString(fragment) {
51
+ if (!(fragment instanceof Html))
52
+ throw new TypeError("htmlString needs Html");
53
+ return Html.markupOf(fragment);
54
+ }
55
+ const ENTITIES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" };
56
+ const escapeText = (text) => text.replace(/[&<>"']/g, char => ENTITIES[char]);
57
+ function render(value) {
58
+ if (value === null || value === undefined || typeof value === "boolean")
59
+ return "";
60
+ if (value instanceof Html)
61
+ return Html.markupOf(value);
62
+ if (Array.isArray(value))
63
+ return value.map(render).join("");
64
+ if (typeof value === "string")
65
+ return escapeText(value);
66
+ if (typeof value === "number" || typeof value === "bigint")
67
+ return String(value);
68
+ throw new TypeError(`html: cannot interpolate ${typeof value}`);
69
+ }
70
+ // A template's literal text is checked once per call site: POST forms and CSRF fields come only from postForm.
71
+ const checked = new WeakSet();
72
+ const POST_FORM = /<form\b[^>]*\bmethod\s*=\s*["']?post\b/i;
73
+ const CSRF_FIELD = /\bname\s*=\s*["']?csrf\b/i;
74
+ function checkLiteral(strings) {
75
+ if (checked.has(strings))
76
+ return;
77
+ const text = strings.join("\u0000");
78
+ if (POST_FORM.test(text))
79
+ throw new Error("html: write POST forms with postForm, which adds the CSRF field");
80
+ if (CSRF_FIELD.test(text))
81
+ throw new Error("html: postForm adds the CSRF field; don't write one");
82
+ checked.add(strings);
83
+ }
84
+ /** Markup with every interpolation escaped as text, except values that are already `Html`. */
85
+ export function html(strings, ...values) {
86
+ checkLiteral(strings);
87
+ let out = strings[0];
88
+ for (let index = 0; index < values.length; index++)
89
+ out += render(values[index]) + strings[index + 1];
90
+ return make(out);
91
+ }
92
+ /** Parts in order, with an optional separator between them. */
93
+ export function joinHtml(parts, separator = "") {
94
+ const between = render(separator);
95
+ return make([...parts].map(render).join(between));
96
+ }
97
+ /** Plain text, escaped (for a sink that takes Html when the content is only words). */
98
+ export const textHtml = (text) => make(escapeText(text));
99
+ /** Attributes as `name="value"` pairs: true is a bare attribute; false, null and undefined are left out. */
100
+ export function attributes(values) {
101
+ let out = "";
102
+ for (const [name, value] of Object.entries(values)) {
103
+ if (!/^[a-z][a-z0-9-]*$/.test(name))
104
+ throw new Error(`attributes: bad name ${name}`);
105
+ if (value === false || value === null || value === undefined)
106
+ continue;
107
+ out += value === true ? ` ${name}` : ` ${name}="${escapeText(String(value))}"`;
108
+ }
109
+ return make(out);
110
+ }
111
+ let formToken = () => undefined;
112
+ /** The server's request context supplies the request's CSRF token (server/request-context.ts). */
113
+ export function provideFormToken(source) { formToken = source; }
114
+ /**
115
+ * A POST form: the action, the request's one CSRF field, the optional
116
+ * project revision and return fields, other hidden fields, then the body.
117
+ * Outside a console request there is no CSRF to give, so it refuses.
118
+ */
119
+ export function postForm(action, body, options = {}) {
120
+ let csrf = null;
121
+ if (options.signedOut !== true) {
122
+ const token = formToken();
123
+ if (token === undefined)
124
+ throw new Error(`postForm(${action}) outside a console request`);
125
+ csrf = token;
126
+ }
127
+ const hidden = [];
128
+ if (csrf !== null)
129
+ hidden.push(["csrf", csrf]);
130
+ if (options.projectRevision !== undefined && options.projectRevision !== null)
131
+ hidden.push(["projectRevision", options.projectRevision]);
132
+ if (options.returnTo !== undefined && options.returnTo !== null)
133
+ hidden.push(["return", options.returnTo]);
134
+ for (const [name, value] of Object.entries(options.hidden ?? {})) {
135
+ if (name === "csrf")
136
+ throw new Error("postForm adds the CSRF field itself");
137
+ if (value !== null && value !== undefined)
138
+ hidden.push([name, value]);
139
+ }
140
+ const fields = hidden.map(([name, value]) => `<input type="hidden" name="${escapeText(name)}" value="${escapeText(String(value))}">`).join("");
141
+ return make(`<form method="post" action="${escapeText(action)}"${htmlString(attributes(options.attrs ?? {}))}>${fields}${render(body)}</form>`);
142
+ }
143
+ /**
144
+ * Markup edited after rendering: each match of `pattern` in the page is replaced by markup the replacer builds
145
+ * (with html`…`), or kept as it was (null), so nothing unescaped can enter. For whole-page passes (a sign-in that
146
+ * stands in for passwords, the browser shell's insertions).
147
+ */
148
+ export function replaceMarkup(page, pattern, replace) {
149
+ const groups = new RegExp(`${pattern.source}|`).exec("").length - 1;
150
+ return make(htmlString(page).replace(pattern, (match, ...rest) => {
151
+ const replaced = replace(match, ...rest.slice(0, groups).map(one => typeof one === "string" ? one : ""));
152
+ return replaced === null ? match : htmlString(replaced);
153
+ }));
154
+ }
155
+ // ---- the page shell's own elements ---------------------------------------------------------------------------
156
+ /** An inline script whose source is code. Its text may not close the element or open a comment. */
157
+ export function scriptElement(source, attrs = {}) {
158
+ if (/<\/script|<!--/i.test(source))
159
+ throw new Error("scriptElement: the source would end its element");
160
+ return make(`<script${htmlString(attributes(attrs))}>${source}</script>`);
161
+ }
162
+ /** A style element whose rules are code. */
163
+ export function styleElement(css, attrs = {}) {
164
+ if (/<\/style/i.test(css))
165
+ throw new Error("styleElement: the rules would end their element");
166
+ return make(`<style${htmlString(attributes(attrs))}>${css}</style>`);
167
+ }
168
+ /** Inert JSON for a page script to read: no character of it can end the element. */
169
+ export function jsonScript(value, attrs = {}) {
170
+ // An escaped "<" can neither close the element nor open a comment; the line separators keep it valid script.
171
+ const json = JSON.stringify(value).replace(/[<\u2028\u2029]/g, char => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
172
+ return make(`<script type="application/json"${htmlString(attributes(attrs))}>${json}</script>`);
173
+ }
@@ -4,10 +4,11 @@
4
4
  * failed, what uses it, and one action. The list comes from saved checks; a
5
5
  * render never waits on one (see integrations.ts).
6
6
  */
7
+ import { type Html } from "./html.js";
7
8
  import { type Integration } from "./integrations.js";
8
9
  export declare const INTEGRATIONS_CSS: string;
9
10
  export declare function integrationsHtml(list: readonly Integration[], csrf: string, notice: {
10
11
  said?: string | null;
11
12
  problem?: string | null;
12
13
  checking?: boolean;
13
- }): string;
14
+ }): Html;
@@ -4,10 +4,10 @@
4
4
  * failed, what uses it, and one action. The list comes from saved checks; a
5
5
  * render never waits on one (see integrations.ts).
6
6
  */
7
+ import { html, joinHtml, postForm } from "./html.js";
7
8
  import { STATE_WORDS } from "./integrations.js";
8
9
  import { whenUtc } from "./when-html.js";
9
10
  import { brandMarkHtml } from "./brand-mark.js";
10
- const e = (value) => String(value ?? "").replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
11
11
  const when = (at) => whenUtc(at);
12
12
  export const INTEGRATIONS_CSS = `.integrations{max-width:760px;min-width:0}.integrations h2{margin:24px 0 6px;font-size:.9375rem}` +
13
13
  `.integrations .integration{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:4px 16px;align-items:start;padding:12px 0;border-top:1px solid var(--so-line);min-width:0}` +
@@ -31,13 +31,12 @@ const GROUPS = [
31
31
  function action(one, csrf) {
32
32
  const a = one.action;
33
33
  if (a.kind === "test") {
34
- return csrf === "" ? "" : `<form method="post" action="/settings/integrations/test" class="integration-action"><input type="hidden" name="csrf" value="${e(csrf)}">` +
35
- `<input type="hidden" name="key" value="${e(one.key)}"><button type="submit">Send test</button></form>`;
34
+ return csrf === "" ? "" : postForm("/settings/integrations/test", html `<button type="submit">Send test</button>`, { attrs: { class: "integration-action" }, hidden: { key: one.key } });
36
35
  }
37
36
  // A command is run where Toolroll runs; a page is linked. Fix and Set up both say which.
38
37
  // Fix is the one filled action on the page; Set up stays quiet so a list of unused services doesn't shout.
39
38
  if (a.href !== null)
40
- return `<p class="integration-action"><a class="button-link${a.kind === "setup" ? " integration-quiet" : ""}" href="${e(a.href)}">${a.label}</a></p>`;
39
+ return html `<p class="integration-action"><a class="button-link${a.kind === "setup" ? " integration-quiet" : ""}" href="${a.href}">${a.label}</a></p>`;
41
40
  return "";
42
41
  }
43
42
  /** Which mark a row shows: email is Gmail's only when the account is a Gmail address; a custom tool is a letter. */
@@ -48,30 +47,27 @@ function markFor(one) {
48
47
  }
49
48
  function row(one, csrf) {
50
49
  const badge = !one.checked && one.state === "connected"
51
- ? `<span class="integration-state integration-state--checking"><i aria-hidden="true"></i>Checking</span>`
52
- : `<span class="integration-state integration-state--${one.state}"><i aria-hidden="true"></i>${STATE_WORDS[one.state]}</span>`;
53
- const facts = [one.account, one.detail].filter(Boolean).map(e).join(" · ");
50
+ ? html `<span class="integration-state integration-state--checking"><i aria-hidden="true"></i>Checking</span>`
51
+ : html `<span class="integration-state integration-state--${one.state}"><i aria-hidden="true"></i>${STATE_WORDS[one.state]}</span>`;
52
+ const facts = [one.account, one.detail].filter(Boolean).join(" · ");
54
53
  const command = one.action.kind !== "test" ? one.action.command : null;
55
- const fix = one.action.kind === "fix" ? `<p class="integration-fix" role="status">${e(one.action.words)}</p>` : "";
56
- const run = command === null || (one.action.kind === "fix" && one.action.words.includes(command)) ? "" : `<p class="meta">Run <code>${e(command)}</code> on the computer running Toolroll.</p>`;
57
- const seen = [one.lastSuccessAt === null ? null : `Last success ${when(one.lastSuccessAt)}`, one.usedBy.length === 0 ? null : `Used by ${e(one.usedBy.join(", "))}`].filter(Boolean);
58
- const history = one.state === "not-set-up" || seen.length === 0 ? "" : `<p class="meta">${seen.join(" · ")}</p>`;
54
+ const fix = one.action.kind === "fix" ? html `<p class="integration-fix" role="status">${one.action.words}</p>` : "";
55
+ const run = command === null || (one.action.kind === "fix" && one.action.words.includes(command)) ? "" : html `<p class="meta">Run <code>${command}</code> on the computer running Toolroll.</p>`;
56
+ const seen = [one.lastSuccessAt === null ? null : html `Last success ${when(one.lastSuccessAt)}`, one.usedBy.length === 0 ? null : html `Used by ${one.usedBy.join(", ")}`].filter(Boolean);
57
+ const history = one.state === "not-set-up" || seen.length === 0 ? "" : html `<p class="meta">${joinHtml(seen, " · ")}</p>`;
59
58
  const lastError = one.lastError === null || one.state === "not-set-up" ? "" : one.state === "broken" && one.action.kind === "fix" && one.action.words === one.lastError
60
- ? `<p class="meta">Failed ${when(one.lastErrorAt ?? "")}</p>`
61
- : `<details><summary>Last error ${one.lastErrorAt === null ? "" : when(one.lastErrorAt)}</summary><p class="meta">${e(one.lastError)}</p></details>`;
62
- return `<div class="integration" data-integration="${e(one.key)}" data-state="${one.state}">` +
63
- `<p class="integration-head">${brandMarkHtml(markFor(one), one.name, one.state === "connected")}<strong>${e(one.name)}</strong> ${badge}</p>` +
64
- action(one, csrf) +
65
- `<div class="integration-body">${facts === "" ? "" : `<p class="meta">${facts}</p>`}${fix}${run}${history}${lastError}</div></div>`;
59
+ ? html `<p class="meta">Failed ${when(one.lastErrorAt ?? "")}</p>`
60
+ : html `<details><summary>Last error ${one.lastErrorAt === null ? "" : when(one.lastErrorAt)}</summary><p class="meta">${one.lastError}</p></details>`;
61
+ return html `<div class="integration" data-integration="${one.key}" data-state="${one.state}"><p class="integration-head">${brandMarkHtml(markFor(one), one.name, one.state === "connected")}<strong>${one.name}</strong> ${badge}</p>${action(one, csrf)}<div class="integration-body">${facts === "" ? "" : html `<p class="meta">${facts}</p>`}${fix}${run}${history}${lastError}</div></div>`;
66
62
  }
67
63
  export function integrationsHtml(list, csrf, notice) {
68
- const note = notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : notice.said ? `<p role="status">${e(notice.said)}</p>` : "";
64
+ const note = notice.problem ? html `<p class="problem" role="alert">${notice.problem}</p>` : notice.said ? html `<p role="status">${notice.said}</p>` : "";
69
65
  const broken = list.filter(one => one.state === "broken").length;
70
- const summary = broken > 0 ? `<p><strong>${broken} need${broken === 1 ? "s" : ""} fixing.</strong></p>`
71
- : notice.checking || list.some(one => !one.checked) ? `<p class="meta" role="status">Checking in the background. Refresh in a few seconds.</p>` : "";
66
+ const summary = broken > 0 ? html `<p><strong>${broken} need${broken === 1 ? "s" : ""} fixing.</strong></p>`
67
+ : notice.checking || list.some(one => !one.checked) ? html `<p class="meta" role="status">Checking in the background. Refresh in a few seconds.</p>` : "";
72
68
  const sections = GROUPS.map(([group, title]) => {
73
69
  const rows = list.filter(one => one.group === group);
74
- return rows.length === 0 ? "" : `<h2>${title}</h2>${rows.map(one => row(one, csrf)).join("")}`;
75
- }).join("");
76
- return `<section class="integrations">${note}${summary}${sections}</section>`;
70
+ return rows.length === 0 ? "" : html `<h2>${title}</h2>${rows.map(one => row(one, csrf))}`;
71
+ });
72
+ return html `<section class="integrations">${note}${summary}${sections}</section>`;
77
73
  }
package/dist/kits-ui.d.ts CHANGED
@@ -5,13 +5,14 @@
5
5
  */
6
6
  import type { Store } from "./store.js";
7
7
  import { type Kit } from "./kits.js";
8
+ import { type Html } from "./html.js";
8
9
  export declare const KITS_CSS: string;
9
10
  /** The gallery: every kit, set up in a project with one button (or opened, when it's already there). */
10
- export declare function kitsGalleryHtml(store: Store, projects: readonly string[], projectName: (repo: string) => string, csrf: string, canSetUp: boolean, notice: {
11
+ export declare function kitsGalleryHtml(store: Store, projects: readonly string[], projectName: (repo: string) => string, canSetUp: boolean, notice: {
11
12
  problem?: string | null;
12
- }): string;
13
+ }): Html;
13
14
  /** A kit's own page: what's set up, and the next step on each line. */
14
- export declare function kitPageHtml(store: Store, kit: Kit, repo: string, dir: string | null, csrf: string, canSetUp: boolean, notice: {
15
+ export declare function kitPageHtml(store: Store, kit: Kit, repo: string, dir: string | null, canSetUp: boolean, notice: {
15
16
  said?: string | null;
16
17
  problem?: string | null;
17
- }): string;
18
+ }): Html;
package/dist/kits-ui.js CHANGED
@@ -1,9 +1,8 @@
1
1
  import { kitChecklist, kitInstalled, KITS } from "./kits.js";
2
2
  import { TEAMMATE_TEMPLATES } from "./teammates.js";
3
- const e = (text) => text.replace(/[&<>"']/g, one => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[one]);
4
- const hidden = (csrf) => `<input type="hidden" name="csrf" value="${e(csrf)}">`;
5
- const CHECK = `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>`;
6
- const RING = `<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8"/></svg>`;
3
+ import { html, postForm } 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>`;
7
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}` +
8
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}` +
9
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}` +
@@ -15,32 +14,31 @@ export const KITS_CSS = `.kits{max-width:960px;min-width:0}.kits-lede{color:var(
15
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}}`;
16
15
  const teammateWords = (kit) => { const template = TEAMMATE_TEMPLATES.find(one => one.id === kit.teammate.template); return template === undefined ? kit.teammate.handle : template.label.toLowerCase(); };
17
16
  /** The gallery: every kit, set up in a project with one button (or opened, when it's already there). */
18
- export function kitsGalleryHtml(store, projects, projectName, csrf, canSetUp, notice) {
17
+ export function kitsGalleryHtml(store, projects, projectName, canSetUp, notice) {
19
18
  const cards = KITS.map(kit => {
20
19
  const here = projects.filter(repo => kitInstalled(store, kit, repo) !== null);
21
20
  const open = projects.length === 1 && here.length === 1;
22
21
  const name = kit.teammate.handle.charAt(0).toUpperCase() + kit.teammate.handle.slice(1);
23
22
  const gets = `${name} (${teammateWords(kit)}) · the ${kit.flowName} flow${kit.tools.length === 0 ? "" : ` · works with ${kit.tools.map(one => one.label).join(" and ")}`}`;
24
- const action = open ? `<a class="kit-open" href="/kits/${kit.id}?repo=${encodeURIComponent(projects[0])}">Open ${e(kit.name)} →</a>`
23
+ const action = open ? html `<a class="kit-open" href="/kits/${kit.id}?repo=${encodeURIComponent(projects[0])}">Open ${kit.name} →</a>`
25
24
  : !canSetUp || projects.length === 0 ? ""
26
- : `<form method="post" action="/kits/${kit.id}/setup">${hidden(csrf)}${projects.length === 1 ? `<input type="hidden" name="repo" value="${e(projects[0])}">`
27
- : `<label>Project<select name="repo">${projects.map(repo => `<option value="${e(repo)}">${e(projectName(repo))}${here.includes(repo) ? " (set up)" : ""}</option>`).join("")}</select></label>`}<button>Set it up</button></form>`;
28
- return `<article class="kit" data-kit="${kit.id}"><h2>${e(kit.name)}</h2><p class="kit-promise">${e(kit.promise)}</p><p class="kit-gets">${e(gets)}</p>${action}</article>`;
29
- }).join("");
30
- return `<section class="kits">${notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : ""}<p class="kits-lede">A working setup in one click: an AI teammate, the flow it works, and what to connect next. Nothing goes out without you approving it.</p><div class="kit-grid">${cards}</div></section>`;
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>`;
28
+ });
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: an AI teammate, the flow it works, and what to connect next. Nothing goes out without you approving it.</p><div class="kit-grid">${cards}</div></section>`;
31
30
  }
32
31
  /** A kit's own page: what's set up, and the next step on each line. */
33
- export function kitPageHtml(store, kit, repo, dir, csrf, canSetUp, notice) {
32
+ export function kitPageHtml(store, kit, repo, dir, canSetUp, notice) {
34
33
  const steps = kitChecklist(store, kit, repo, dir);
35
34
  const set = kitInstalled(store, kit, repo);
36
35
  const rows = steps.map(step => {
37
- const go = step.done ? (step.href === null ? "" : `<a class="kit-step-go" href="${e(step.href)}">Open</a>`)
36
+ const go = step.done ? (step.href === null ? "" : html `<a class="kit-step-go" href="${step.href}">Open</a>`)
38
37
  : step.action === "sample" || step.action === "github"
39
- ? (canSetUp ? `<form method="post" action="/kits/${kit.id}/${step.action}">${hidden(csrf)}<input type="hidden" name="repo" value="${e(repo)}"><button${step.action === "sample" ? "" : ' class="secondary"'}>${step.action === "sample" ? "Try it" : "Bring them in"}</button></form>` : "")
40
- : step.href === null ? "" : `<a class="kit-step-go" href="${e(step.href)}">${step.action === "connect" ? "Connect" : "Set up"}</a>`;
41
- return `<li data-step="${e(step.id)}" data-done="${step.done}">${step.done ? CHECK : RING}<span class="kit-step-said">${e(step.said)}</span>${go}</li>`;
42
- }).join("");
43
- const setUp = set === null && canSetUp ? `<form method="post" action="/kits/${kit.id}/setup">${hidden(csrf)}<input type="hidden" name="repo" value="${e(repo)}"><button>Set up ${e(kit.name)}</button></form>` : "";
44
- return `<section class="kits">${notice.problem ? `<p class="problem" role="alert">${e(notice.problem)}</p>` : ""}${notice.said ? `<p class="said" role="status">${e(notice.said)}</p>` : ""}` +
45
- `<p class="kits-lede">${e(kit.promise)}</p>${setUp}${rows === "" ? "" : `<ol class="kit-steps">${rows}</ol>`}<p class="meta"><a href="/kits">All starter kits</a></p></section>`;
38
+ ? (canSetUp ? postForm(`/kits/${kit.id}/${step.action}`, html `<button${step.action === "sample" ? "" : html ` class="secondary"`}>${step.action === "sample" ? "Try it" : "Bring them in"}</button>`, { hidden: { repo } }) : "")
39
+ : step.href === null ? "" : html `<a class="kit-step-go" href="${step.href}">${step.action === "connect" ? "Connect" : "Set up"}</a>`;
40
+ 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>`;
41
+ });
42
+ const setUp = set === null && canSetUp ? postForm(`/kits/${kit.id}/setup`, html `<button>Set up ${kit.name}</button>`, { hidden: { repo } }) : "";
43
+ 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>`;
46
44
  }
@@ -1,14 +1,15 @@
1
1
  import { type KnowledgeDraft, type KnowledgeSelection, type KnowledgeView } from './project-knowledge.js';
2
+ import { type Html } from './html.js';
2
3
  export declare const KNOWLEDGE_CSS = ".knowledge{max-width:780px;min-width:0;overflow-wrap:anywhere}.knowledge .card{padding:20px;min-width:0;margin:16px 0}.knowledge h2{margin:0 0 12px}.knowledge form{display:grid;gap:12px;margin:0}.knowledge label{display:grid;gap:6px}.knowledge textarea,.knowledge input,.knowledge select{box-sizing:border-box;width:100%;min-width:0;max-width:100%}.knowledge textarea{resize:vertical;min-height:110px}.knowledge button,.knowledge summary,.knowledge .button-link{min-height:44px}.knowledge button{justify-self:start;white-space:nowrap}.knowledge summary{padding:12px 0;cursor:pointer}.knowledge pre{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit}.knowledge .knowledge-actions{display:flex;gap:12px;flex-wrap:wrap}.knowledge .meta{font-size:.86rem}.knowledge .knowledge-source{display:block}.knowledge details+details{border-top:1px solid var(--so-line)}.memory-stats{display:flex;flex-wrap:wrap;gap:.4rem}.memory-stats span{padding:.15rem .55rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}@media(max-width:600px){.knowledge .card{padding:16px}.knowledge h2{font-size:1.1rem}}";
3
- export declare function knowledgeHtml(view: KnowledgeView, csrf: string, canManage: boolean, draft?: KnowledgeDraft, error?: string): string;
4
- export declare function knowledgeContextHtml(snapshot: KnowledgeSelection | null): string;
4
+ export declare function knowledgeHtml(view: KnowledgeView, csrf: string, canManage: boolean, draft?: KnowledgeDraft, error?: string): Html;
5
+ export declare function knowledgeContextHtml(snapshot: KnowledgeSelection | null): Html | '';
5
6
  import type { Decision, MemoryHit } from './project-memory.js';
6
7
  export declare const MEMORY_INTRO = "What the lead and crew read before they work. Every change keeps its history.";
7
8
  export declare function decisionsHtml(repo: string, decisions: Decision[], csrf: string, canManage: boolean, draft?: {
8
9
  claim?: string;
9
10
  why?: string;
10
- }, error?: string): string;
11
- export declare function memorySearchHtml(repo: string, query: string, hits: MemoryHit[] | null): string;
11
+ }, error?: string): Html;
12
+ export declare function memorySearchHtml(repo: string, query: string, hits: MemoryHit[] | null): Html;
12
13
  import type { MemoryProposal } from './memory-pass.js';
13
14
  export declare function proposalsHtml(repo: string, proposals: MemoryProposal[], status: {
14
15
  surface: string;
@@ -21,4 +22,4 @@ export declare function proposalsHtml(repo: string, proposals: MemoryProposal[],
21
22
  };
22
23
  openGaps: number;
23
24
  pending: number;
24
- } | null, csrf: string, canManage: boolean): string;
25
+ } | null, csrf: string, canManage: boolean): Html;
@@ -1,47 +1,48 @@
1
1
  import { STALE_KNOWLEDGE } from './project-knowledge.js';
2
- const e = (s) => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
3
- const fields = (data) => Object.entries(data).map(([k, v]) => `<input type="hidden" name="${e(k)}" value="${e(v)}">`).join('');
2
+ import { html, postForm } from './html.js';
3
+ // Hidden fields as they always were: a missing value is an empty field.
4
+ const fields = (data) => Object.fromEntries(Object.entries(data).map(([k, v]) => [k, String(v ?? '')]));
4
5
  export const KNOWLEDGE_CSS = `.knowledge{max-width:780px;min-width:0;overflow-wrap:anywhere}.knowledge .card{padding:20px;min-width:0;margin:16px 0}.knowledge h2{margin:0 0 12px}.knowledge form{display:grid;gap:12px;margin:0}.knowledge label{display:grid;gap:6px}.knowledge textarea,.knowledge input,.knowledge select{box-sizing:border-box;width:100%;min-width:0;max-width:100%}.knowledge textarea{resize:vertical;min-height:110px}.knowledge button,.knowledge summary,.knowledge .button-link{min-height:44px}.knowledge button{justify-self:start;white-space:nowrap}.knowledge summary{padding:12px 0;cursor:pointer}.knowledge pre{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit}.knowledge .knowledge-actions{display:flex;gap:12px;flex-wrap:wrap}.knowledge .meta{font-size:.86rem}.knowledge .knowledge-source{display:block}.knowledge details+details{border-top:1px solid var(--so-line)}.memory-stats{display:flex;flex-wrap:wrap;gap:.4rem}.memory-stats span{padding:.15rem .55rem;border-radius:.375rem;background:var(--so-neutral-soft);color:var(--so-neutral-ink)}@media(max-width:600px){.knowledge .card{padding:16px}.knowledge h2{font-size:1.1rem}}`;
5
6
  export function knowledgeHtml(view, csrf, canManage, draft, error) {
6
7
  const manage = canManage && !!csrf;
7
- const base = { csrf, repo: view.repo, identity: view.identity, revision: view.revision };
8
- const form = (action, body, extra = {}) => `<form method="post" action="/settings/knowledge/change">${fields({ ...base, action, ...extra })}${body}</form>`;
9
- const referenceFields = (d) => `<label>Name<input name="title" maxlength="120" value="${e(d.title)}" required></label><label>Reference text<textarea name="content" rows="5" maxlength="12000">${e(d.content)}</textarea></label><details${d.path ? ' open' : ''}><summary>Use a project document</summary><label>Committed .md or .txt path<input name="path" value="${e(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>`;
10
- const refs = view.knowledge.references.map(ref => `<details class="card"${draft?.id === ref.id ? ' open' : ''}><summary><strong>${e(ref.title)}</strong></summary>${ref.path ? `<span class="meta knowledge-source">${e(ref.path)} · ${e(ref.sourceRevision?.slice(0, 7))}</span>` : ''}<pre>${e(ref.content)}</pre>${manage ? `<details${draft?.id === ref.id ? ' 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', '<p>Remove from future tasks. Previous run context stays available.</p><button>Remove reference</button>', { id: ref.id })}</details></details>` : ''}</details>`).join('');
11
- const history = view.history.map(h => `<li><a class="button-link" href="/settings/knowledge?repo=${encodeURIComponent(view.repo)}&version=${h.revision}">Version ${h.revision}</a> · ${e(h.actor)} · ${e(h.at.slice(0, 16).replace('T', ' '))} UTC</li>`).join('');
8
+ const base = { repo: view.repo, identity: view.identity, revision: view.revision };
9
+ const form = (action, body, extra = {}) => postForm('/settings/knowledge/change', body, { hidden: fields({ ...base, action, ...extra }) });
10
+ 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>`;
11
+ 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>`);
12
+ 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>`);
12
13
  if (view.stale) {
13
14
  // One notice, the saved knowledge on demand, one approver action. Editing waits until it is applied.
14
15
  const saved = view.stale.knowledge;
15
- const review = `<details><summary>Review saved knowledge</summary><pre class="knowledge-instructions">${e(saved.instructions || 'No project instructions.')}</pre>${saved.references.map(ref => `<details><summary>${e(ref.title)}</summary><pre>${e(ref.content)}</pre></details>`).join('')}</details>`;
16
- return `<section class="knowledge knowledge-library">${error ? `<p class="problem" role="alert">${e(error)}</p>` : ''}<section class="card knowledge-stale"><p role="status">${e(STALE_KNOWLEDGE)}.</p>${review}${manage ? form('apply', '<button>Apply saved knowledge</button>') : ''}</section></section>`;
16
+ 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>`;
17
+ 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>`;
17
18
  }
18
- const instructions = `<pre class="knowledge-instructions">${e(view.knowledge.instructions || 'No project instructions yet.')}</pre>`;
19
- const editor = manage ? `<details class="knowledge-editor"${draft?.instructions !== undefined ? ' open' : ''}><summary>${view.knowledge.instructions ? 'Edit instructions' : 'Add instructions'}</summary>${form('instructions', `<textarea aria-label="Project instructions" name="instructions" rows="5" maxlength="4000" placeholder="Goals, preferences, and what good work looks like…">${e(draft?.instructions ?? view.knowledge.instructions)}</textarea><button>Save instructions</button>`)}</details>` : '';
20
- return `<section class="knowledge knowledge-library">${error ? `<p class="problem" role="alert">${e(error)}</p>` : ''}<section class="knowledge-intro"><h2>Instructions</h2>${instructions}${editor}</section><section><h2>References</h2>${refs || '<p class="meta">Add a spec, design guide, or useful project document.</p>'}${manage ? `<details class="card"${draft?.title && !draft.id ? ' open' : ''}><summary>Add reference</summary>${form('save', referenceFields(draft?.id ? {} : draft ?? {}))}</details>` : ''}</section>${history ? `<details><summary>Change history</summary><ol>${history}</ol></details>` : ''}</section>`;
19
+ const instructions = html `<pre class="knowledge-instructions">${view.knowledge.instructions || 'No project instructions yet.'}</pre>`;
20
+ 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>` : '';
21
+ return html `<section class="knowledge knowledge-library">${error ? html `<p class="problem" role="alert">${error}</p>` : ''}<section class="knowledge-intro"><h2>Instructions</h2>${instructions}${editor}</section><section><h2>References</h2>${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>` : ''}</section>${history.length ? html `<details><summary>Change history</summary><ol>${history}</ol></details>` : ''}</section>`;
21
22
  }
22
23
  export function knowledgeContextHtml(snapshot) {
23
24
  if (!snapshot)
24
25
  return '';
25
- return `<details class="knowledge result-knowledge"><summary>Context used</summary><p class="meta">Project knowledge · version ${snapshot.revision}${snapshot.inheritedFrom === null ? '' : ` · from run #${snapshot.inheritedFrom}`}</p>${snapshot.instructions ? `<details><summary>Instructions</summary><pre>${e(snapshot.instructions)}</pre></details>` : '<p>No project instructions were supplied.</p>'}${snapshot.references.map(r => `<details><summary>${e(r.title)}</summary>${r.path ? `<p class="meta">${e(r.path)} · ${e(r.sourceRevision)}</p>` : ''}<pre>${e(r.content)}</pre></details>`).join('')}${snapshot.omitted.length ? `<details><summary>Not included (${snapshot.omitted.length})</summary><ul>${snapshot.omitted.map(r => `<li>${e(r.title)} — ${e(r.reason)}</li>`).join('')}</ul></details>` : ''}</details>`;
26
+ return html `<details class="knowledge result-knowledge"><summary>Context used</summary><p class="meta">Project knowledge · version ${snapshot.revision}${snapshot.inheritedFrom === null ? '' : ` · from run #${snapshot.inheritedFrom}`}</p>${snapshot.instructions ? html `<details><summary>Instructions</summary><pre>${snapshot.instructions}</pre></details>` : html `<p>No project instructions were supplied.</p>`}${snapshot.references.map(r => html `<details><summary>${r.title}</summary>${r.path ? html `<p class="meta">${r.path} · ${r.sourceRevision}</p>` : ''}<pre>${r.content}</pre></details>`)}${snapshot.omitted.length ? html `<details><summary>Not included (${snapshot.omitted.length})</summary><ul>${snapshot.omitted.map(r => html `<li>${r.title} — ${r.reason}</li>`)}</ul></details>` : ''}</details>`;
26
27
  }
27
28
  export const MEMORY_INTRO = 'What the lead and crew read before they work. Every change keeps its history.';
28
29
  export function decisionsHtml(repo, decisions, csrf, canManage, draft, error) {
29
30
  const manage = canManage && !!csrf;
30
- const base = { csrf, repo };
31
- const form = (action, body, extra = {}) => `<form method="post" action="/settings/knowledge/decision">${fields({ ...base, action, ...extra })}${body}</form>`;
32
- const rows = decisions.map(d => `<details class="card decision" id="decision-${d.id}"><summary><strong>${e(d.claim)}</strong> <span class="meta">#${d.id} · ${e(d.decidedBy)} · ${e(d.decidedAt.slice(0, 10))}</span></summary><p>${e(d.why)}</p><p class="meta">${e(d.sourceKind)}${d.sourceRef ? ` · ${e(d.sourceRef)}` : ''}${d.supersedes === null ? '' : ` · replaces #${d.supersedes}`}</p>${manage ? form('retire', `<label>Why it no longer holds<input name="reason" maxlength="500" required></label><button type="submit">Retire</button>`, { decision: d.id }) : ''}</details>`).join('');
33
- const add = manage ? `<details class="knowledge-editor"${draft?.claim !== undefined ? ' open' : ''}><summary>Record a decision</summary>${form('record', `<label>The choice, in one sentence<input name="claim" maxlength="240" value="${e(draft?.claim)}" required></label><label>Why<textarea name="why" rows="3" maxlength="2000" required>${e(draft?.why)}</textarea></label><button type="submit">Record</button>`)}</details>` : '';
34
- return `<section class="knowledge"><h2>Decisions</h2>${error ? `<p class="problem" role="alert">${e(error)}</p>` : ''}${rows || '<p class="meta">No decisions yet. The lead suggests one when a conversation settles a choice.</p>'}${add}</section>`;
31
+ const base = { repo };
32
+ const form = (action, body, extra = {}) => postForm('/settings/knowledge/decision', body, { hidden: fields({ ...base, action, ...extra }) });
33
+ 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>`);
34
+ 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>` : '';
35
+ return html `<section class="knowledge"><h2>Decisions</h2>${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}</section>`;
35
36
  }
36
37
  export function memorySearchHtml(repo, query, hits) {
37
- const kind = (h) => h.kind === 'decision' ? `<a href="#decision-${e(h.ref)}">decision #${e(h.ref)}</a>` : h.kind === 'conversation' ? 'conversation' : h.kind;
38
- const results = hits === null ? '' : hits.length === 0 ? '<p class="meta">Nothing in project memory matches.</p>' : `<ul class="memory-hits">${hits.map(h => `<li><strong>${kind(h)}</strong> · ${e(h.title)}<br><span class="meta">${e(h.snippet)}</span></li>`).join('')}</ul>`;
39
- return `<section class="knowledge memory-search"><form method="get" action="/settings/knowledge"><input type="hidden" name="repo" value="${e(repo)}"><label>Search project memory<input name="memory" value="${e(query)}" maxlength="300" placeholder="Search decisions, references, lessons…" type="search" autocomplete="off"></label><button type="submit">Search</button></form>${results}</section>`;
38
+ const kind = (h) => h.kind === 'decision' ? html `<a href="#decision-${h.ref}">decision #${h.ref}</a>` : h.kind === 'conversation' ? 'conversation' : h.kind;
39
+ 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>`;
40
+ return html `<section class="knowledge memory-search"><form method="get" action="/settings/knowledge"><input type="hidden" name="repo" value="${repo}"><label>Search project memory<input name="memory" value="${query}" maxlength="300" placeholder="Search decisions, references, lessons…" type="search" autocomplete="off"></label><button type="submit">Search</button></form>${results}</section>`;
40
41
  }
41
42
  export function proposalsHtml(repo, proposals, status, csrf, canManage) {
42
43
  const manage = canManage && !!csrf;
43
- const line = status === null ? '' : `<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>` +
44
- `<details class="settings-more"><summary>Read new sessions</summary><p class="meta">Run <code>toolroll memory propose --repo ${e(repo)}</code> on this computer.</p></details>`;
45
- const cards = proposals.map(p => `<details class="card proposal" open><summary><strong>${e(p.title)}</strong> <span class="meta">#${p.id} · ${e(p.kind)} · ${p.sessions} sessions</span></summary><p>${e(p.rationale)}</p>${p.beforeText ? `<pre class="knowledge-instructions">- ${e(p.beforeText)}</pre>` : ''}${p.afterText ? `<pre class="knowledge-instructions">+ ${e(p.afterText)}</pre>` : ''}<ul class="memory-evidence">${p.evidence.map(ev => `<li>“${e(ev.quote.slice(0, 200))}” <span class="meta">${e(ev.session.split(':')[0] ?? '')}</span></li>`).join('')}</ul>${manage ? `<form method="post" action="/settings/knowledge/proposal" class="inline">${fields({ csrf, repo, proposal: p.id, decision: 'accept' })}<button type="submit">Accept</button></form> <form method="post" action="/settings/knowledge/proposal" class="inline">${fields({ csrf, repo, proposal: p.id, decision: 'reject' })}<button type="submit">Reject</button></form>` : ''}</details>`).join('');
46
- return `<section class="knowledge"><h2>Proposed from sessions</h2>${line}${cards || '<p class="meta">Nothing to review. Suggestions appear when the same mistake shows up in two sessions.</p>'}</section>`;
44
+ 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>`;
45
+ 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 }) });
46
+ 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>`);
47
+ return html `<section class="knowledge"><h2>Proposed from sessions</h2>${line}${cards.length ? cards : html `<p class="meta">Nothing to review. Suggestions appear when the same mistake shows up in two sessions.</p>`}</section>`;
47
48
  }