@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.
- package/README.md +85 -6
- package/agents/clarify.meta.json +1 -0
- package/agents/memoryDefragmenter.meta.json +2 -1
- package/agents/reviewer.meta.json +60 -0
- package/agents/worca-cc-code-reviewer.md +33 -0
- package/agents/worca-cc-memory-defragmenter.md +5 -3
- package/agents/workspaceScanner.meta.json +1 -0
- package/package.json +14 -10
- package/scripts/git-diff.mjs +25 -0
- package/scripts/gitDiff.meta.json +18 -0
- package/scripts/js-inline.mjs +11 -0
- package/scripts/js.meta.json +22 -0
- package/scripts/py-inline.py +27 -0
- package/scripts/py.meta.json +22 -0
- package/scripts/shell.meta.json +24 -0
- package/skills/worca/SKILL.md +3 -2
- package/src/cli/models.mjs +247 -0
- package/src/cli/render.mjs +72 -4
- package/src/cli/schedule.mjs +494 -0
- package/src/cli/worca-cc.mjs +1001 -22
- package/src/core/agent-registry.mjs +75 -23
- package/src/core/agent-store.mjs +51 -2
- package/src/core/artifacts.mjs +73 -10
- package/src/core/ask/events.mjs +119 -1
- package/src/core/ask/limits.mjs +32 -4
- package/src/core/ask/mcp-stdio.mjs +12 -0
- package/src/core/ask/model-deps.mjs +126 -0
- package/src/core/ask/model-proposal.mjs +370 -0
- package/src/core/ask/models.mjs +12 -0
- package/src/core/ask/policy-deps.mjs +124 -0
- package/src/core/ask/policy-proposal.mjs +363 -0
- package/src/core/ask/prompt.mjs +74 -9
- package/src/core/ask/proposal.mjs +54 -5
- package/src/core/ask/schedule-deps.mjs +83 -0
- package/src/core/ask/schedule-spec.mjs +310 -0
- package/src/core/ask/script-deps.mjs +357 -0
- package/src/core/ask/source-deps.mjs +52 -0
- package/src/core/ask/source-spec.mjs +157 -0
- package/src/core/ask/spawn.mjs +1 -0
- package/src/core/ask/store.mjs +6 -3
- package/src/core/ask/tool-deps.mjs +4 -0
- package/src/core/ask/tools.mjs +657 -37
- package/src/core/ask/turn.mjs +109 -2
- package/src/core/ask-files.mjs +406 -0
- package/src/core/ask-forms.mjs +195 -0
- package/src/core/ask-projection.mjs +72 -0
- package/src/core/bridge/errors.mjs +84 -0
- package/src/core/bridge/provider-ops.mjs +281 -0
- package/src/core/bridge/providers/copilot.mjs +269 -0
- package/src/core/bridge/providers/endpoint.mjs +257 -0
- package/src/core/bridge/registry.mjs +88 -0
- package/src/core/bridge/semaphore.mjs +73 -0
- package/src/core/bridge/server.mjs +184 -0
- package/src/core/bridge/telemetry.mjs +53 -0
- package/src/core/bridge/translate/request.mjs +252 -0
- package/src/core/bridge/translate/response.mjs +82 -0
- package/src/core/bridge/translate/stream.mjs +242 -0
- package/src/core/bridge/upstream.mjs +209 -0
- package/src/core/chat/command-router.mjs +58 -4
- package/src/core/chat/notifier.mjs +14 -1
- package/src/core/chat/renderers.mjs +35 -0
- package/src/core/claude-runner.mjs +126 -19
- package/src/core/config.mjs +212 -31
- package/src/core/cost-budget.mjs +3 -2
- package/src/core/db.mjs +169 -15
- package/src/core/failure-policy.mjs +10 -0
- package/src/core/fs-browse.mjs +16 -4
- package/src/core/git-info.mjs +22 -0
- package/src/core/graph/builtin-workflows.mjs +3 -1
- package/src/core/graph/exec-io.mjs +71 -0
- package/src/core/graph/executor.mjs +139 -70
- package/src/core/graph/human-evidence.mjs +131 -0
- package/src/core/graph/python-probe.mjs +172 -0
- package/src/core/graph/registry-ports.mjs +10 -6
- package/src/core/graph/scheduler.mjs +39 -24
- package/src/core/graph/script-child.mjs +81 -0
- package/src/core/graph/script-runner.mjs +597 -0
- package/src/core/graph/worca_script.py +207 -0
- package/src/core/guardrail-store.mjs +16 -0
- package/src/core/human-backfill.mjs +108 -0
- package/src/core/human-rate.mjs +17 -0
- package/src/core/index-html.mjs +6 -2
- package/src/core/memory-defrag-model.mjs +112 -0
- package/src/core/memory-store.mjs +70 -18
- package/src/core/memory-sync.mjs +22 -11
- package/src/core/metrics/read.mjs +4 -1
- package/src/core/metrics/record.mjs +50 -2
- package/src/core/metrics/sync.mjs +6 -4
- package/src/core/model-env.mjs +149 -0
- package/src/core/model-test.mjs +14 -1
- package/src/core/notifications.mjs +128 -0
- package/src/core/onboarding.mjs +8 -2
- package/src/core/orchestrator.mjs +357 -26
- package/src/core/phases.mjs +95 -6
- package/src/core/plugin-api.mjs +24 -7
- package/src/core/plugin-manifest.mjs +184 -18
- package/src/core/plugin-models.mjs +1 -0
- package/src/core/plugin-script-cases.mjs +118 -0
- package/src/core/plugin-store.mjs +163 -17
- package/src/core/plugin-workflows.mjs +71 -17
- package/src/core/policy/cache.mjs +116 -0
- package/src/core/policy/effective.mjs +175 -0
- package/src/core/policy/gate.mjs +91 -0
- package/src/core/policy/local.mjs +145 -0
- package/src/core/policy/registry.mjs +330 -0
- package/src/core/policy/scope.mjs +61 -0
- package/src/core/policy/state.mjs +79 -0
- package/src/core/policy/sync.mjs +513 -0
- package/src/core/protocol.mjs +43 -0
- package/src/core/run-harness.mjs +421 -72
- package/src/core/scheduler.mjs +980 -0
- package/src/core/script-bench.mjs +628 -0
- package/src/core/script-registry.mjs +116 -0
- package/src/core/script-store.mjs +563 -0
- package/src/core/settings.mjs +489 -14
- package/src/core/stats.mjs +33 -2
- package/src/core/workflow-export.mjs +94 -3
- package/src/core/workflow-share.mjs +67 -18
- package/src/core/workflows.mjs +47 -11
- package/src/core/workspaces.mjs +18 -12
- package/src/shared/forms/answer.mjs +164 -0
- package/src/shared/forms/catalog.mjs +91 -0
- package/src/shared/forms/form-def.mjs +290 -0
- package/src/shared/forms/layout.mjs +67 -0
- package/src/shared/forms/paths.mjs +47 -0
- package/src/shared/forms/project.mjs +309 -0
- package/src/shared/forms/schema.mjs +205 -0
- package/src/shared/graph/agent-meta.mjs +55 -5
- package/src/shared/graph/constants.mjs +14 -2
- package/src/shared/graph/flow-layout.mjs +2 -1
- package/src/shared/graph/isomorphic.mjs +5 -3
- package/src/shared/graph/manifest.mjs +22 -13
- package/src/shared/graph/ports.mjs +45 -19
- package/src/shared/graph/script-cases.mjs +257 -0
- package/src/shared/graph/script-icons.mjs +46 -0
- package/src/shared/graph/script-infer.mjs +259 -0
- package/src/shared/graph/script-meta.mjs +408 -0
- package/src/shared/graph/script-templates.mjs +201 -0
- package/src/shared/graph/template.mjs +4 -4
- package/src/shared/graph/validate.mjs +89 -16
- package/src/shared/human-estimate.mjs +100 -0
- package/src/shared/schedule/recurrence.mjs +353 -0
- package/src/shared/team-metrics/aggregate.mjs +51 -11
- package/{scripts → tools}/install.mjs +3 -3
- package/ui/public/app.js +4390 -683
- package/ui/public/artifact-picker.mjs +189 -0
- package/ui/public/ask/dom.mjs +121 -0
- package/ui/public/ask/form-preview.mjs +55 -0
- package/ui/public/ask/form-renderer.mjs +250 -0
- package/ui/public/ask/registry.mjs +53 -0
- package/ui/public/ask/widgets-display.mjs +370 -0
- package/ui/public/ask/widgets-input.mjs +624 -0
- package/ui/public/ask/widgets-layout.mjs +90 -0
- package/ui/public/ask-panel.mjs +401 -27
- package/ui/public/ask-run-card.mjs +1 -1
- package/ui/public/bridge-view.mjs +694 -0
- package/ui/public/chat-settings-view.mjs +24 -0
- package/ui/public/code-editor.mjs +181 -0
- package/ui/public/getting-started.mjs +34 -7
- package/ui/public/graph/composer.mjs +138 -10
- package/ui/public/graph/inspector.mjs +61 -58
- package/ui/public/graph/palette.mjs +27 -9
- package/ui/public/graph/run-decor.mjs +34 -17
- package/ui/public/graph/run-hosts.mjs +7 -1
- package/ui/public/graph/save-dialog.mjs +3 -0
- package/ui/public/graph/view.mjs +23 -7
- package/ui/public/guardrails-view.mjs +15 -3
- package/ui/public/guide-spot.mjs +87 -9
- package/ui/public/index.html +480 -141
- package/ui/public/memory-view.mjs +22 -4
- package/ui/public/models-view.mjs +162 -17
- package/ui/public/node-tunables.mjs +33 -4
- package/ui/public/plugins-view.mjs +23 -1
- package/ui/public/results-view.mjs +4 -2
- package/ui/public/schedule-sheet.mjs +430 -0
- package/ui/public/schedules-view.mjs +432 -0
- package/ui/public/script-bench-view.mjs +1154 -0
- package/ui/public/script-forms.mjs +282 -0
- package/ui/public/script-wizard.mjs +529 -0
- package/ui/public/scripts-view.mjs +868 -0
- package/ui/public/stats-view.mjs +159 -52
- package/ui/public/style.css +1461 -44
- package/ui/public/team-metrics-surfaces.mjs +77 -16
- package/ui/public/team-metrics-view.mjs +68 -5
- package/ui/public/team-policy-view.mjs +1402 -0
- package/ui/public/ui-level.mjs +237 -0
- 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
|
+
});
|