@worca/app 1.3.0 → 1.4.0

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 (187) hide show
  1. package/README.md +85 -6
  2. package/agents/clarify.meta.json +1 -0
  3. package/agents/memoryDefragmenter.meta.json +2 -1
  4. package/agents/reviewer.meta.json +60 -0
  5. package/agents/worca-cc-code-reviewer.md +33 -0
  6. package/agents/worca-cc-memory-defragmenter.md +5 -3
  7. package/agents/workspaceScanner.meta.json +1 -0
  8. package/package.json +14 -10
  9. package/scripts/git-diff.mjs +25 -0
  10. package/scripts/gitDiff.meta.json +18 -0
  11. package/scripts/js-inline.mjs +11 -0
  12. package/scripts/js.meta.json +22 -0
  13. package/scripts/py-inline.py +27 -0
  14. package/scripts/py.meta.json +22 -0
  15. package/scripts/shell.meta.json +24 -0
  16. package/skills/worca/SKILL.md +3 -2
  17. package/src/cli/models.mjs +247 -0
  18. package/src/cli/render.mjs +72 -4
  19. package/src/cli/schedule.mjs +494 -0
  20. package/src/cli/worca-cc.mjs +1001 -22
  21. package/src/core/agent-registry.mjs +75 -23
  22. package/src/core/agent-store.mjs +51 -2
  23. package/src/core/artifacts.mjs +73 -10
  24. package/src/core/ask/events.mjs +119 -1
  25. package/src/core/ask/limits.mjs +32 -4
  26. package/src/core/ask/mcp-stdio.mjs +12 -0
  27. package/src/core/ask/model-deps.mjs +126 -0
  28. package/src/core/ask/model-proposal.mjs +370 -0
  29. package/src/core/ask/models.mjs +12 -0
  30. package/src/core/ask/policy-deps.mjs +124 -0
  31. package/src/core/ask/policy-proposal.mjs +363 -0
  32. package/src/core/ask/prompt.mjs +74 -9
  33. package/src/core/ask/proposal.mjs +54 -5
  34. package/src/core/ask/schedule-deps.mjs +83 -0
  35. package/src/core/ask/schedule-spec.mjs +310 -0
  36. package/src/core/ask/script-deps.mjs +357 -0
  37. package/src/core/ask/source-deps.mjs +52 -0
  38. package/src/core/ask/source-spec.mjs +157 -0
  39. package/src/core/ask/spawn.mjs +1 -0
  40. package/src/core/ask/store.mjs +6 -3
  41. package/src/core/ask/tool-deps.mjs +4 -0
  42. package/src/core/ask/tools.mjs +657 -37
  43. package/src/core/ask/turn.mjs +109 -2
  44. package/src/core/ask-files.mjs +406 -0
  45. package/src/core/ask-forms.mjs +195 -0
  46. package/src/core/ask-projection.mjs +72 -0
  47. package/src/core/bridge/errors.mjs +84 -0
  48. package/src/core/bridge/provider-ops.mjs +281 -0
  49. package/src/core/bridge/providers/copilot.mjs +269 -0
  50. package/src/core/bridge/providers/endpoint.mjs +257 -0
  51. package/src/core/bridge/registry.mjs +88 -0
  52. package/src/core/bridge/semaphore.mjs +73 -0
  53. package/src/core/bridge/server.mjs +184 -0
  54. package/src/core/bridge/telemetry.mjs +53 -0
  55. package/src/core/bridge/translate/request.mjs +252 -0
  56. package/src/core/bridge/translate/response.mjs +82 -0
  57. package/src/core/bridge/translate/stream.mjs +242 -0
  58. package/src/core/bridge/upstream.mjs +209 -0
  59. package/src/core/chat/command-router.mjs +58 -4
  60. package/src/core/chat/notifier.mjs +14 -1
  61. package/src/core/chat/renderers.mjs +35 -0
  62. package/src/core/claude-runner.mjs +126 -19
  63. package/src/core/config.mjs +212 -31
  64. package/src/core/cost-budget.mjs +3 -2
  65. package/src/core/db.mjs +169 -15
  66. package/src/core/failure-policy.mjs +10 -0
  67. package/src/core/fs-browse.mjs +16 -4
  68. package/src/core/git-info.mjs +22 -0
  69. package/src/core/graph/builtin-workflows.mjs +3 -1
  70. package/src/core/graph/exec-io.mjs +71 -0
  71. package/src/core/graph/executor.mjs +139 -70
  72. package/src/core/graph/human-evidence.mjs +131 -0
  73. package/src/core/graph/python-probe.mjs +172 -0
  74. package/src/core/graph/registry-ports.mjs +10 -6
  75. package/src/core/graph/scheduler.mjs +39 -24
  76. package/src/core/graph/script-child.mjs +81 -0
  77. package/src/core/graph/script-runner.mjs +597 -0
  78. package/src/core/graph/worca_script.py +207 -0
  79. package/src/core/guardrail-store.mjs +16 -0
  80. package/src/core/human-backfill.mjs +108 -0
  81. package/src/core/human-rate.mjs +17 -0
  82. package/src/core/index-html.mjs +6 -2
  83. package/src/core/memory-defrag-model.mjs +112 -0
  84. package/src/core/memory-store.mjs +70 -18
  85. package/src/core/memory-sync.mjs +22 -11
  86. package/src/core/metrics/read.mjs +4 -1
  87. package/src/core/metrics/record.mjs +50 -2
  88. package/src/core/metrics/sync.mjs +6 -4
  89. package/src/core/model-env.mjs +149 -0
  90. package/src/core/model-test.mjs +14 -1
  91. package/src/core/notifications.mjs +128 -0
  92. package/src/core/onboarding.mjs +8 -2
  93. package/src/core/orchestrator.mjs +357 -26
  94. package/src/core/phases.mjs +95 -6
  95. package/src/core/plugin-api.mjs +24 -7
  96. package/src/core/plugin-manifest.mjs +184 -18
  97. package/src/core/plugin-models.mjs +1 -0
  98. package/src/core/plugin-script-cases.mjs +118 -0
  99. package/src/core/plugin-store.mjs +163 -17
  100. package/src/core/plugin-workflows.mjs +71 -17
  101. package/src/core/policy/cache.mjs +116 -0
  102. package/src/core/policy/effective.mjs +175 -0
  103. package/src/core/policy/gate.mjs +91 -0
  104. package/src/core/policy/local.mjs +145 -0
  105. package/src/core/policy/registry.mjs +330 -0
  106. package/src/core/policy/scope.mjs +61 -0
  107. package/src/core/policy/state.mjs +79 -0
  108. package/src/core/policy/sync.mjs +513 -0
  109. package/src/core/protocol.mjs +43 -0
  110. package/src/core/run-harness.mjs +421 -72
  111. package/src/core/scheduler.mjs +980 -0
  112. package/src/core/script-bench.mjs +628 -0
  113. package/src/core/script-registry.mjs +116 -0
  114. package/src/core/script-store.mjs +563 -0
  115. package/src/core/settings.mjs +489 -14
  116. package/src/core/stats.mjs +33 -2
  117. package/src/core/workflow-export.mjs +94 -3
  118. package/src/core/workflow-share.mjs +67 -18
  119. package/src/core/workflows.mjs +47 -11
  120. package/src/core/workspaces.mjs +18 -12
  121. package/src/shared/forms/answer.mjs +164 -0
  122. package/src/shared/forms/catalog.mjs +91 -0
  123. package/src/shared/forms/form-def.mjs +290 -0
  124. package/src/shared/forms/layout.mjs +67 -0
  125. package/src/shared/forms/paths.mjs +47 -0
  126. package/src/shared/forms/project.mjs +309 -0
  127. package/src/shared/forms/schema.mjs +205 -0
  128. package/src/shared/graph/agent-meta.mjs +55 -5
  129. package/src/shared/graph/constants.mjs +14 -2
  130. package/src/shared/graph/flow-layout.mjs +2 -1
  131. package/src/shared/graph/isomorphic.mjs +5 -3
  132. package/src/shared/graph/manifest.mjs +22 -13
  133. package/src/shared/graph/ports.mjs +45 -19
  134. package/src/shared/graph/script-cases.mjs +257 -0
  135. package/src/shared/graph/script-icons.mjs +46 -0
  136. package/src/shared/graph/script-infer.mjs +259 -0
  137. package/src/shared/graph/script-meta.mjs +408 -0
  138. package/src/shared/graph/script-templates.mjs +201 -0
  139. package/src/shared/graph/template.mjs +4 -4
  140. package/src/shared/graph/validate.mjs +89 -16
  141. package/src/shared/human-estimate.mjs +100 -0
  142. package/src/shared/schedule/recurrence.mjs +353 -0
  143. package/src/shared/team-metrics/aggregate.mjs +51 -11
  144. package/{scripts → tools}/install.mjs +3 -3
  145. package/ui/public/app.js +4390 -683
  146. package/ui/public/artifact-picker.mjs +189 -0
  147. package/ui/public/ask/dom.mjs +121 -0
  148. package/ui/public/ask/form-preview.mjs +55 -0
  149. package/ui/public/ask/form-renderer.mjs +250 -0
  150. package/ui/public/ask/registry.mjs +53 -0
  151. package/ui/public/ask/widgets-display.mjs +370 -0
  152. package/ui/public/ask/widgets-input.mjs +624 -0
  153. package/ui/public/ask/widgets-layout.mjs +90 -0
  154. package/ui/public/ask-panel.mjs +401 -27
  155. package/ui/public/ask-run-card.mjs +1 -1
  156. package/ui/public/bridge-view.mjs +694 -0
  157. package/ui/public/chat-settings-view.mjs +24 -0
  158. package/ui/public/code-editor.mjs +181 -0
  159. package/ui/public/getting-started.mjs +34 -7
  160. package/ui/public/graph/composer.mjs +138 -10
  161. package/ui/public/graph/inspector.mjs +61 -58
  162. package/ui/public/graph/palette.mjs +27 -9
  163. package/ui/public/graph/run-decor.mjs +34 -17
  164. package/ui/public/graph/run-hosts.mjs +7 -1
  165. package/ui/public/graph/save-dialog.mjs +3 -0
  166. package/ui/public/graph/view.mjs +23 -7
  167. package/ui/public/guardrails-view.mjs +15 -3
  168. package/ui/public/guide-spot.mjs +87 -9
  169. package/ui/public/index.html +480 -141
  170. package/ui/public/memory-view.mjs +22 -4
  171. package/ui/public/models-view.mjs +162 -17
  172. package/ui/public/node-tunables.mjs +33 -4
  173. package/ui/public/plugins-view.mjs +23 -1
  174. package/ui/public/results-view.mjs +4 -2
  175. package/ui/public/schedule-sheet.mjs +430 -0
  176. package/ui/public/schedules-view.mjs +432 -0
  177. package/ui/public/script-bench-view.mjs +1154 -0
  178. package/ui/public/script-forms.mjs +282 -0
  179. package/ui/public/script-wizard.mjs +529 -0
  180. package/ui/public/scripts-view.mjs +868 -0
  181. package/ui/public/stats-view.mjs +159 -52
  182. package/ui/public/style.css +1461 -44
  183. package/ui/public/team-metrics-surfaces.mjs +77 -16
  184. package/ui/public/team-metrics-view.mjs +68 -5
  185. package/ui/public/team-policy-view.mjs +1402 -0
  186. package/ui/public/ui-level.mjs +237 -0
  187. package/ui/server.mjs +1827 -73
@@ -0,0 +1,370 @@
1
+ // ui/public/ask/widgets-display.mjs
2
+ // The DISPLAY half of the ask widget catalog v1 (ask-forms design §6.1). These
3
+ // widgets add nothing to the answer and are skipped by gate 3. Every agent string
4
+ // reaches the DOM through textContent; markdown is the one exception and it goes
5
+ // through ctx.markdown — the app's existing marked + DOMPurify pipeline, injected
6
+ // by the host — never through innerHTML.
7
+ import { resolvePath } from '../../../src/shared/forms/paths.mjs';
8
+ import { h, icon, ICON_INFO, fmtBytes, extOf, toneOf, own, fileRefAt, fileRefOfBind, noFileTile, servable } from './dom.mjs';
9
+ import { cellFor, colsOf, galleryPicker } from './widgets-input.mjs';
10
+
11
+ /** The value an item's `bind` points at, or undefined. */
12
+ function bound(item, ctx) {
13
+ return item.bind ? resolvePath(item.bind, { data: ctx.data }) : undefined;
14
+ }
15
+
16
+ /** A display widget's optional caption row, drawn like a field label. */
17
+ function labelled(ctx, item, body) {
18
+ if (!item.label) return body;
19
+ const wrap = h(ctx.doc, 'div', 'af-fld af-display');
20
+ wrap.append(h(ctx.doc, 'div', 'af-label', item.label), body);
21
+ return wrap;
22
+ }
23
+
24
+ /** The "Open" affordance a servable file widget carries (and the PDF fallback, W14). */
25
+ function openLink(ctx, entry) {
26
+ const a = h(ctx.doc, 'a', 'af-open', 'Open');
27
+ a.setAttribute('href', ctx.fileUrl(entry.index));
28
+ a.setAttribute('target', '_blank');
29
+ a.setAttribute('rel', 'noopener noreferrer');
30
+ return a;
31
+ }
32
+
33
+ /**
34
+ * W5 — the ONE text seam. The widget never fetches: it asks the host for the
35
+ * file's text and paints on resolve, unless the form was disposed meanwhile.
36
+ * `fill(text)` is the widget's own inline painter, so a file-bound widget and an
37
+ * inline one draw identically.
38
+ */
39
+ function fillFromFile(ctx, entry, fill) {
40
+ if (!ctx.loadText) return;
41
+ ctx.loadText(entry.index).then((text) => {
42
+ if (ctx.isDisposed()) return;
43
+ fill(String(text == null ? '' : text));
44
+ }).catch(() => { /* a missing preview is not a failed answer */ });
45
+ }
46
+
47
+ /** Minimal RFC-4180 CSV -> rows of objects, for a `table` bound to a .csv file. */
48
+ function csvRows(text) {
49
+ const lines = String(text).split(/\r?\n/).filter((l) => l !== '');
50
+ if (!lines.length) return { head: [], rows: [] };
51
+ const split = (line) => {
52
+ const out = []; let cur = ''; let q = false;
53
+ for (let i = 0; i < line.length; i += 1) {
54
+ const c = line[i];
55
+ if (q) {
56
+ if (c === '"' && line[i + 1] === '"') { cur += '"'; i += 1; }
57
+ else if (c === '"') q = false;
58
+ else cur += c;
59
+ } else if (c === '"') q = true;
60
+ else if (c === ',') { out.push(cur); cur = ''; }
61
+ else cur += c;
62
+ }
63
+ out.push(cur);
64
+ return out;
65
+ };
66
+ const head = split(lines[0]);
67
+ const rows = lines.slice(1).map((l) => {
68
+ const cells = split(l);
69
+ const row = {};
70
+ head.forEach((k, i) => { row[k] = cells[i]; });
71
+ return row;
72
+ });
73
+ return { head, rows };
74
+ }
75
+
76
+ function markdownWidget(item, ctx) {
77
+ const ref = fileRefOfBind(item, ctx);
78
+ const entry = ref && servable(ctx, ref.entry) ? ref.entry : null;
79
+ if (ref && !entry) return labelled(ctx, item, noFileTile(ctx, ref.rel));
80
+ const box = h(ctx.doc, 'div', 'af-md');
81
+ if (!entry) { // inline markdown, the common case
82
+ ctx.trackMarkdown(box, String(bound(item, ctx) ?? ''));
83
+ return labelled(ctx, item, box);
84
+ }
85
+ const host = h(ctx.doc, 'div', 'af-md-host');
86
+ const bar = h(ctx.doc, 'div', 'af-file-bar');
87
+ bar.append(h(ctx.doc, 'span', 'af-file-name', entry.name), openLink(ctx, entry));
88
+ host.append(bar, box); // the body stays empty until the text lands
89
+ fillFromFile(ctx, entry, (text) => ctx.trackMarkdown(box, text));
90
+ return labelled(ctx, item, host);
91
+ }
92
+
93
+ function codeLike(item, ctx, diff) {
94
+ const ref = fileRefOfBind(item, ctx);
95
+ const entry = ref && servable(ctx, ref.entry) ? ref.entry : null;
96
+ if (ref && !entry) return labelled(ctx, item, noFileTile(ctx, ref.rel));
97
+ const host = h(ctx.doc, 'div', 'af-code-host');
98
+ const paint = (text) => {
99
+ const box = textBox(ctx, text, { name: item.name || (entry && entry.name), lang: diff ? '' : item.lang, diff });
100
+ if (entry) box.querySelector('.af-code-bar').appendChild(openLink(ctx, entry));
101
+ host.replaceChildren(box);
102
+ };
103
+ paint(entry ? '' : bound(item, ctx));
104
+ if (entry) fillFromFile(ctx, entry, paint);
105
+ return labelled(ctx, item, host);
106
+ }
107
+
108
+ function codeWidget(item, ctx) { return codeLike(item, ctx, false); }
109
+ function diffWidget(item, ctx) { return codeLike(item, ctx, true); }
110
+
111
+ function tableWidget(item, ctx) {
112
+ const ref = fileRefOfBind(item, ctx);
113
+ const entry = ref && servable(ctx, ref.entry) ? ref.entry : null;
114
+ if (ref && !entry) return labelled(ctx, item, noFileTile(ctx, ref.rel));
115
+ const wrap = h(ctx.doc, 'div', 'af-tbl-wrap');
116
+ const paint = (rows, cols) => {
117
+ const table = h(ctx.doc, 'table', 'af-tbl');
118
+ const thead = ctx.doc.createElement('thead');
119
+ const hr = ctx.doc.createElement('tr');
120
+ for (const c of cols) hr.appendChild(h(ctx.doc, 'th', c.align === 'right' ? 'af-r' : '', c.label || c.key));
121
+ thead.appendChild(hr);
122
+ const tbody = ctx.doc.createElement('tbody');
123
+ for (const r of rows) {
124
+ const tr = ctx.doc.createElement('tr');
125
+ for (const c of cols) tr.appendChild(cellFor(ctx, c, r));
126
+ tbody.appendChild(tr);
127
+ }
128
+ table.append(thead, tbody);
129
+ wrap.replaceChildren(table);
130
+ };
131
+ const declared = colsOf(item);
132
+ if (entry) {
133
+ paint([], declared);
134
+ fillFromFile(ctx, entry, (text) => {
135
+ const { head, rows } = csvRows(text);
136
+ paint(rows, declared.length ? declared : head.map((k) => ({ key: k, label: k })));
137
+ });
138
+ } else {
139
+ const rows = bound(item, ctx);
140
+ paint(Array.isArray(rows) ? rows : [], declared);
141
+ }
142
+ return labelled(ctx, item, wrap);
143
+ }
144
+
145
+ function jsonWidget(item, ctx) {
146
+ const ref = fileRefOfBind(item, ctx);
147
+ const entry = ref && servable(ctx, ref.entry) ? ref.entry : null;
148
+ if (ref && !entry) return labelled(ctx, item, noFileTile(ctx, ref.rel));
149
+ const box = h(ctx.doc, 'div', 'af-json');
150
+ const paint = (value) => box.replaceChildren(jsonNode(ctx, null, value));
151
+ if (entry) {
152
+ paint(null);
153
+ fillFromFile(ctx, entry, (text) => {
154
+ let parsed = null;
155
+ try { parsed = JSON.parse(text); } catch { parsed = null; }
156
+ paint(parsed);
157
+ });
158
+ } else {
159
+ paint(bound(item, ctx));
160
+ }
161
+ return labelled(ctx, item, box);
162
+ }
163
+
164
+ function calloutWidget(item, ctx) {
165
+ const tone = toneOf(item.tone) || 'info';
166
+ const box = h(ctx.doc, 'div', `af-callout af-${tone}`);
167
+ box.appendChild(icon(ctx.doc, ICON_INFO, 16));
168
+ const body = h(ctx.doc, 'div', 'af-callout-body');
169
+ if (item.title) body.appendChild(h(ctx.doc, 'b', '', item.title));
170
+ body.appendChild(ctx.doc.createTextNode(String(bound(item, ctx) ?? item.text ?? '')));
171
+ box.appendChild(body);
172
+ return box;
173
+ }
174
+
175
+ function jsonNode(ctx, key, v) {
176
+ const k = key == null ? null : h(ctx.doc, 'span', 'af-j-k', `${key}: `);
177
+ if (v && typeof v === 'object') {
178
+ const arr = Array.isArray(v);
179
+ const keys = Object.keys(v);
180
+ const d = ctx.doc.createElement('details');
181
+ d.open = true;
182
+ const sum = ctx.doc.createElement('summary');
183
+ if (k) sum.appendChild(k);
184
+ sum.appendChild(h(ctx.doc, 'span', 'af-j-c', arr ? `[${keys.length}]` : `{${keys.length}}`));
185
+ d.appendChild(sum);
186
+ for (const kk of keys) d.appendChild(jsonNode(ctx, arr ? null : kk, v[kk]));
187
+ return d;
188
+ }
189
+ const leaf = h(ctx.doc, 'div', 'af-j-leaf');
190
+ if (k) leaf.appendChild(k);
191
+ leaf.appendChild(h(ctx.doc, 'span', typeof v === 'string' ? 'af-j-s' : 'af-j-n', JSON.stringify(v)));
192
+ return leaf;
193
+ }
194
+
195
+ /**
196
+ * The shared code / diff chrome: a bar, then one `.af-ln` span per source line.
197
+ * `diff: true` classes +/- / @@ lines and counts them into the bar.
198
+ */
199
+ export function textBox(ctx, text, { name = '', lang = '', diff = false } = {}) {
200
+ const src = String(text == null ? '' : text);
201
+ const wrap = h(ctx.doc, 'div', 'af-code');
202
+ const bar = h(ctx.doc, 'div', 'af-code-bar');
203
+ bar.appendChild(h(ctx.doc, 'span', 'af-code-name', name || (diff ? 'changes' : 'snippet')));
204
+ const lines = src === '' ? [] : src.split('\n');
205
+ if (diff) {
206
+ let add = 0; let del = 0;
207
+ for (const l of lines) { if (l[0] === '+') add += 1; else if (l[0] === '-') del += 1; }
208
+ bar.append(h(ctx.doc, 'span', 'af-add', `+${add}`), h(ctx.doc, 'span', 'af-del', `−${del}`));
209
+ } else if (lang) {
210
+ bar.appendChild(h(ctx.doc, 'span', 'af-code-lang', lang));
211
+ }
212
+ const pre = ctx.doc.createElement('pre');
213
+ const code = ctx.doc.createElement('code');
214
+ if (!diff && lang) code.className = `language-${String(lang).replace(/[^A-Za-z0-9_+-]/g, '')}`;
215
+ if (diff) {
216
+ lines.forEach((l) => {
217
+ const cls = l[0] === '+' ? 'af-ln af-add' : l[0] === '-' ? 'af-ln af-del'
218
+ : l.slice(0, 2) === '@@' ? 'af-ln af-hunk' : 'af-ln';
219
+ code.appendChild(h(ctx.doc, 'span', cls, l));
220
+ });
221
+ } else {
222
+ code.textContent = src; // byte-exact, so hljs can round-trip it
223
+ }
224
+ pre.appendChild(code);
225
+ wrap.append(bar, pre);
226
+ if (!diff && lang && ctx.highlight) { try { void ctx.highlight(wrap); } catch { /* cosmetic */ } }
227
+ return wrap;
228
+ }
229
+
230
+ function fileListWidget(item, ctx) {
231
+ const rows = bound(item, ctx);
232
+ const fileKey = item.fileKey || 'file';
233
+ const noteKey = item.noteKey || 'note';
234
+ const ul = h(ctx.doc, 'ul', 'af-files');
235
+ for (const r of Array.isArray(rows) ? rows : []) {
236
+ // X16: a row widget reads its own declared `fileKey` — no fileRefs lookup needed.
237
+ const rel = own(r, fileKey);
238
+ const entry = ctx.fileFor(rel);
239
+ const li = ctx.doc.createElement('li');
240
+ // X14: no snapshot -> the row IS the .af-nofile tile (badge + name, no size).
241
+ if (!entry) li.className = 'af-nofile';
242
+ li.appendChild(h(ctx.doc, 'span', 'af-file-badge', extOf(entry ? entry.name : rel)));
243
+ li.appendChild(h(ctx.doc, 'span', 'af-file-name', String(rel == null ? '' : rel)));
244
+ const note = own(r, noteKey);
245
+ if (note != null && note !== '') li.appendChild(h(ctx.doc, 'span', 'af-file-note', String(note)));
246
+ if (entry) li.appendChild(h(ctx.doc, 'span', 'af-file-size', fmtBytes(entry.bytes)));
247
+ ul.appendChild(li);
248
+ }
249
+ return labelled(ctx, item, ul);
250
+ }
251
+
252
+ function imageWidget(item, ctx) {
253
+ const ref = fileRefOfBind(item, ctx); // X16: declared as type:'file'?
254
+ const entry = ref && servable(ctx, ref.entry) ? ref.entry : null;
255
+ if (!entry) return labelled(ctx, item, noFileTile(ctx, ref ? ref.rel : item.bind)); // X14
256
+ const fig = h(ctx.doc, 'figure', 'af-img');
257
+ const img = ctx.doc.createElement('img'); // SVG included
258
+ img.setAttribute('src', ctx.fileUrl(entry.index));
259
+ img.setAttribute('alt', String(item.caption || (entry && entry.name) || ''));
260
+ img.setAttribute('loading', 'lazy');
261
+ fig.appendChild(img);
262
+ const cap = h(ctx.doc, 'figcaption', '');
263
+ cap.textContent = `${item.caption ? `${item.caption} — ` : ''}${entry.name} · ${fmtBytes(entry.bytes)}`;
264
+ cap.appendChild(openLink(ctx, entry));
265
+ fig.appendChild(cap);
266
+ return labelled(ctx, item, fig);
267
+ }
268
+
269
+ function compareWidget(item, ctx) {
270
+ const beforeRef = fileRefAt(item.before, ctx); // X16: by DECLARED path
271
+ const afterRef = fileRefAt(item.after, ctx);
272
+ const before = beforeRef && beforeRef.entry;
273
+ const after = afterRef && afterRef.entry;
274
+ const beforeLabel = String(item.beforeLabel || 'Before');
275
+ const afterLabel = String(item.afterLabel || 'After');
276
+ // X14: either half missing makes the swipe meaningless — one tile per side.
277
+ if (!servable(ctx, before) || !servable(ctx, after)) {
278
+ const box = h(ctx.doc, 'div', 'af-cmp');
279
+ box.append(noFileTile(ctx, beforeRef ? beforeRef.rel : item.before),
280
+ noFileTile(ctx, afterRef ? afterRef.rel : item.after));
281
+ return labelled(ctx, item, box);
282
+ }
283
+ const view = h(ctx.doc, 'div', 'af-cmp-view');
284
+ const baseImg = ctx.doc.createElement('img');
285
+ baseImg.setAttribute('src', ctx.fileUrl(after.index));
286
+ baseImg.setAttribute('alt', afterLabel);
287
+ const top = ctx.doc.createElement('img');
288
+ top.className = 'af-cmp-top';
289
+ top.setAttribute('src', ctx.fileUrl(before.index));
290
+ top.setAttribute('alt', beforeLabel);
291
+ const line = h(ctx.doc, 'div', 'af-cmp-line');
292
+ view.append(baseImg, top, line,
293
+ h(ctx.doc, 'span', 'af-cmp-tag af-l', beforeLabel),
294
+ h(ctx.doc, 'span', 'af-cmp-tag af-r', afterLabel));
295
+ const set = (p) => {
296
+ top.style.clipPath = `inset(0 ${100 - p}% 0 0)`;
297
+ line.style.left = `${p}%`;
298
+ };
299
+ const r = ctx.doc.createElement('input');
300
+ r.type = 'range';
301
+ r.className = 'af-cmp-range';
302
+ r.setAttribute('min', '0');
303
+ r.setAttribute('max', '100');
304
+ r.setAttribute('step', '1');
305
+ r.value = '50';
306
+ r.id = ctx.idFor(`cmp-${item.before || ''}`);
307
+ r.setAttribute('aria-label', `Swipe between ${beforeLabel} and ${afterLabel}`);
308
+ r.addEventListener('input', () => set(Number(r.value)));
309
+ set(50);
310
+ const box = h(ctx.doc, 'div', 'af-cmp');
311
+ box.append(view, r);
312
+ return labelled(ctx, item, box);
313
+ }
314
+
315
+ function mediaWidget(item, ctx) {
316
+ const ref = fileRefOfBind(item, ctx); // X16
317
+ const entry = ref && servable(ctx, ref.entry) ? ref.entry : null;
318
+ if (!entry) return labelled(ctx, item, noFileTile(ctx, ref ? ref.rel : item.bind)); // X14
319
+ const audio = String(entry.mime || '').startsWith('audio/');
320
+ const el = ctx.doc.createElement(audio ? 'audio' : 'video');
321
+ el.className = 'af-media';
322
+ el.setAttribute('controls', '');
323
+ el.setAttribute('preload', 'metadata'); // never autoplay
324
+ el.setAttribute('src', ctx.fileUrl(entry.index));
325
+ el.setAttribute('aria-label', entry.name);
326
+ return labelled(ctx, item, el);
327
+ }
328
+
329
+ /**
330
+ * W14 — the browser's own PDF viewer in a host-owned frame. The frame carries NO
331
+ * `sandbox` attribute on purpose: Chromium refuses to run its PDF plugin inside a
332
+ * sandboxed frame, and the bytes are type-verified server side (%PDF- sniff,
333
+ * allowlist, nosniff, Content-Disposition: inline) on a loopback-guarded route.
334
+ * The "Open" link is permanent, so a browser that will not embed a PDF still has
335
+ * a way through. To fall back to link-only: drop the `frame` from the append.
336
+ */
337
+ function pdfWidget(item, ctx) {
338
+ const ref = fileRefOfBind(item, ctx); // X16
339
+ const entry = ref && servable(ctx, ref.entry) ? ref.entry : null;
340
+ if (!entry) return labelled(ctx, item, noFileTile(ctx, ref ? ref.rel : item.bind, 'PDF')); // X14: no frame
341
+ const box = h(ctx.doc, 'div', 'af-pdf');
342
+ const bar = h(ctx.doc, 'div', 'af-pdf-bar');
343
+ bar.append(h(ctx.doc, 'span', 'af-file-badge', 'PDF'),
344
+ h(ctx.doc, 'span', 'af-file-name', entry.name),
345
+ h(ctx.doc, 'span', 'af-file-size', fmtBytes(entry.bytes)),
346
+ openLink(ctx, entry));
347
+ const frame = ctx.doc.createElement('iframe');
348
+ frame.className = 'af-pdf-frame';
349
+ frame.dataset.afPdf = 'frame';
350
+ frame.setAttribute('title', `PDF preview: ${entry.name}`);
351
+ frame.setAttribute('loading', 'lazy');
352
+ frame.setAttribute('src', ctx.fileUrl(entry.index));
353
+ box.append(bar, frame);
354
+ return labelled(ctx, item, box);
355
+ }
356
+
357
+ export const DISPLAY_WIDGETS_TABLE = Object.freeze({
358
+ markdown: markdownWidget,
359
+ callout: calloutWidget,
360
+ table: tableWidget,
361
+ json: jsonWidget,
362
+ code: codeWidget,
363
+ diff: diffWidget,
364
+ 'file-list': fileListWidget,
365
+ image: imageWidget,
366
+ gallery: galleryPicker, // the SAME function; `item.field` makes it an input
367
+ compare: compareWidget,
368
+ media: mediaWidget,
369
+ pdf: pdfWidget,
370
+ });