@desplega.ai/agent-swarm 1.78.1 → 1.79.1

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 (75) hide show
  1. package/README.md +1 -0
  2. package/openapi.json +1335 -236
  3. package/package.json +4 -4
  4. package/plugin/skills/artifacts/SKILL.md +151 -0
  5. package/plugin/skills/artifacts/examples/static-report.sh +1 -1
  6. package/plugin/skills/kv-storage/SKILL.md +168 -0
  7. package/plugin/skills/pages/SKILL.md +423 -0
  8. package/src/artifact-sdk/browser-sdk.ts +396 -19
  9. package/src/be/db.ts +548 -0
  10. package/src/be/migrations/059_pages.sql +34 -0
  11. package/src/be/migrations/060_page_versions.sql +19 -0
  12. package/src/be/migrations/061_kv_store.sql +34 -0
  13. package/src/be/migrations/062_pages_view_count.sql +9 -0
  14. package/src/commands/artifact.ts +17 -11
  15. package/src/commands/provider-credentials.ts +1 -1
  16. package/src/http/index.ts +9 -1
  17. package/src/http/kv.ts +658 -0
  18. package/src/http/page-proxy.ts +213 -0
  19. package/src/http/pages-public.ts +507 -0
  20. package/src/http/pages.ts +608 -0
  21. package/src/http/status.ts +1 -1
  22. package/src/http/utils.ts +68 -5
  23. package/src/pages/version.ts +44 -0
  24. package/src/prompts/session-templates.ts +51 -0
  25. package/src/providers/pi-mono-adapter.ts +3 -3
  26. package/src/providers/pi-mono-extension.ts +1 -1
  27. package/src/server.ts +29 -1
  28. package/src/tasks/context-key.ts +28 -0
  29. package/src/telemetry.ts +65 -1
  30. package/src/tests/artifact-commands.test.ts +92 -0
  31. package/src/tests/artifact-sdk.test.ts +80 -74
  32. package/src/tests/context-key.test.ts +17 -0
  33. package/src/tests/create-page-tool.test.ts +197 -0
  34. package/src/tests/fixtures/sample-json-page.json +52 -0
  35. package/src/tests/kv-http.test.ts +331 -0
  36. package/src/tests/kv-namespace-resolution.test.ts +172 -0
  37. package/src/tests/kv-page-proxy.test.ts +212 -0
  38. package/src/tests/kv-storage.test.ts +227 -0
  39. package/src/tests/kv-tool.test.ts +217 -0
  40. package/src/tests/launch-password-rejection.test.ts +139 -0
  41. package/src/tests/page-proxy-authed.test.ts +146 -0
  42. package/src/tests/page-proxy.test.ts +270 -0
  43. package/src/tests/page-session.test.ts +169 -0
  44. package/src/tests/pages-actions-endpoint.test.ts +102 -0
  45. package/src/tests/pages-authed-mode.test.ts +211 -0
  46. package/src/tests/pages-http.test.ts +193 -0
  47. package/src/tests/pages-list-endpoint.test.ts +149 -0
  48. package/src/tests/pages-password-hash.test.ts +57 -0
  49. package/src/tests/pages-password-mode.test.ts +265 -0
  50. package/src/tests/pages-public-authed-401.test.ts +102 -0
  51. package/src/tests/pages-public-html.test.ts +151 -0
  52. package/src/tests/pages-public-json-redirect.test.ts +86 -0
  53. package/src/tests/pages-storage.test.ts +196 -0
  54. package/src/tests/pages-versioning.test.ts +231 -0
  55. package/src/tests/pages-view-count.test.ts +220 -0
  56. package/src/tests/prompt-template-session.test.ts +3 -2
  57. package/src/tests/skill-update-scope.test.ts +165 -0
  58. package/src/tests/swarm-diff.test.ts +303 -0
  59. package/src/tests/telemetry-init.test.ts +149 -0
  60. package/src/tests/workflow-wait-event.test.ts +4 -7
  61. package/src/tools/create-page.ts +263 -0
  62. package/src/tools/kv/index.ts +5 -0
  63. package/src/tools/kv/kv-delete.ts +89 -0
  64. package/src/tools/kv/kv-get.ts +64 -0
  65. package/src/tools/kv/kv-incr.ts +116 -0
  66. package/src/tools/kv/kv-list.ts +81 -0
  67. package/src/tools/kv/kv-set.ts +194 -0
  68. package/src/tools/kv/resolve-namespace.ts +58 -0
  69. package/src/tools/skills/skill-update.ts +26 -0
  70. package/src/tools/tool-config.ts +10 -0
  71. package/src/types.ts +107 -0
  72. package/src/utils/internal-ai/complete-structured.ts +2 -2
  73. package/src/utils/internal-ai/credentials.ts +3 -3
  74. package/src/utils/page-session.ts +254 -0
  75. package/plugin/skills/artifacts/skill.md +0 -70
@@ -1,29 +1,406 @@
1
- // This is a string template that gets served as JavaScript to the browser
1
+ // Browser-side Swarm SDK injected into agent-served HTML pages.
2
+ //
3
+ // Exposes a domain-grouped API on `window.SwarmSDK` (class) and a ready-to-use
4
+ // singleton `window.swarmSdk`. All calls route through the `/@swarm/api/*`
5
+ // proxy, which strips the page-session cookie and forwards to `/api/*` with
6
+ // a server-side bearer + agent-id. From the page's perspective, the SDK is
7
+ // authenticated automatically — no token handling on the client.
8
+ //
9
+ // Domains exposed:
10
+ // - tasks create, list, get, storeProgress
11
+ // - agents list, get
12
+ // - events create, list, batch, counts
13
+ // - memory search, list, get, rate
14
+ // - repos list, get, create, update, delete
15
+ // - schedules list, get, create, update, delete, run
16
+ // - approvalRequests list, get, create, respond
17
+ // - kv get, set, del, incr, list (namespace is forced server-
18
+ // side to the page's own `task:page:<id>` — no namespace
19
+ // argument is exposed)
20
+ //
21
+ // Full HTTP API reference: https://docs.agent-swarm.dev/docs/api-reference
2
22
  export const BROWSER_SDK_JS = `
3
23
  class SwarmSDK {
4
24
  constructor() {
5
- this._configPromise = fetch('/@swarm/config').then(r => r.json());
6
- }
25
+ this._configPromise = fetch('/@swarm/config').then(r => r.json()).catch(() => null);
7
26
 
8
- async createTask(opts) { return this._post('/@swarm/api/tasks', opts); }
9
- async getTasks(filters) { return this._get('/@swarm/api/tasks?' + new URLSearchParams(filters)); }
10
- async getTaskDetails(id) { return this._get('/@swarm/api/tasks/' + id); }
11
- async storeProgress(taskId, data) { return this._post('/@swarm/api/tasks/' + taskId + '/progress', data); }
12
- async postMessage(opts) { return this._post('/@swarm/api/messages', opts); }
13
- async readMessages(opts) { return this._get('/@swarm/api/messages?' + new URLSearchParams(opts)); }
14
- async getSwarm() { return this._get('/@swarm/api/agents'); }
15
- async listServices() { return this._get('/@swarm/api/services'); }
16
- async slackReply(opts) { return this._post('/@swarm/api/slack/reply', opts); }
27
+ const base = '/@swarm/api';
28
+ const call = async (method, path, body) => {
29
+ const init = { method };
30
+ if (body !== undefined) {
31
+ init.headers = { 'Content-Type': 'application/json' };
32
+ init.body = JSON.stringify(body);
33
+ }
34
+ const res = await fetch(base + path, init);
35
+ const text = await res.text();
36
+ let parsed = null;
37
+ if (text) {
38
+ try { parsed = JSON.parse(text); } catch { parsed = text; }
39
+ }
40
+ if (!res.ok) {
41
+ const err = new Error('SwarmSDK ' + method + ' ' + path + ': ' + res.status);
42
+ err.status = res.status;
43
+ err.response = parsed;
44
+ throw err;
45
+ }
46
+ return parsed;
47
+ };
48
+ const qs = (obj) => {
49
+ if (!obj) return '';
50
+ const p = new URLSearchParams();
51
+ for (const [k, v] of Object.entries(obj)) {
52
+ if (v === undefined || v === null) continue;
53
+ p.set(k, String(v));
54
+ }
55
+ const s = p.toString();
56
+ return s ? '?' + s : '';
57
+ };
58
+ const enc = encodeURIComponent;
17
59
 
18
- async _post(url, body) {
19
- const res = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
20
- return res.json();
21
- }
22
- async _get(url) {
23
- const res = await fetch(url);
24
- return res.json();
60
+ this.tasks = {
61
+ create: (body) => call('POST', '/tasks', body),
62
+ list: (filters) => call('GET', '/tasks' + qs(filters)),
63
+ get: (id) => call('GET', '/tasks/' + enc(id)),
64
+ storeProgress: (id, data) => call('POST', '/tasks/' + enc(id) + '/progress', data),
65
+ };
66
+
67
+ this.agents = {
68
+ list: () => call('GET', '/agents'),
69
+ get: (id) => call('GET', '/agents/' + enc(id)),
70
+ };
71
+
72
+ this.events = {
73
+ create: (body) => call('POST', '/events', body),
74
+ list: (filters) => call('GET', '/events' + qs(filters)),
75
+ batch: (body) => call('POST', '/events/batch', body),
76
+ counts: (filters) => call('GET', '/events/counts' + qs(filters)),
77
+ };
78
+
79
+ this.memory = {
80
+ search: (body) => call('POST', '/memory/search', body),
81
+ list: (filters) => call('GET', '/memory/list' + qs(filters)),
82
+ get: (id) => call('GET', '/memory/' + enc(id)),
83
+ rate: (body) => call('POST', '/memory/rate', body),
84
+ };
85
+
86
+ this.repos = {
87
+ list: () => call('GET', '/repos'),
88
+ get: (id) => call('GET', '/repos/' + enc(id)),
89
+ create: (body) => call('POST', '/repos', body),
90
+ update: (id, body) => call('PUT', '/repos/' + enc(id), body),
91
+ delete: (id) => call('DELETE', '/repos/' + enc(id)),
92
+ };
93
+
94
+ this.schedules = {
95
+ list: () => call('GET', '/schedules'),
96
+ get: (id) => call('GET', '/schedules/' + enc(id)),
97
+ create: (body) => call('POST', '/schedules', body),
98
+ update: (id, body) => call('PUT', '/schedules/' + enc(id), body),
99
+ delete: (id) => call('DELETE', '/schedules/' + enc(id)),
100
+ run: (id) => call('POST', '/schedules/' + enc(id) + '/run'),
101
+ };
102
+
103
+ this.approvalRequests = {
104
+ list: (filters) => call('GET', '/approval-requests' + qs(filters)),
105
+ get: (id) => call('GET', '/approval-requests/' + enc(id)),
106
+ create: (body) => call('POST', '/approval-requests', body),
107
+ respond: (id, body) => call('POST', '/approval-requests/' + enc(id) + '/respond', body),
108
+ };
109
+
110
+ // KV store. The namespace is FORCED by the page-proxy to \`task:page:<id>\`
111
+ // (it injects X-Page-Id which the kv handler treats as highest priority).
112
+ // No namespace argument is exposed — pages cannot read/write any other
113
+ // namespace via this SDK.
114
+ this.kv = {
115
+ get: (key) => call('GET', '/kv/' + enc(key)),
116
+ set: (key, value, opts) => call('PUT', '/kv/' + enc(key), {
117
+ value,
118
+ valueType: opts && opts.valueType,
119
+ expiresInSec: opts && opts.expiresInSec,
120
+ }),
121
+ del: (key) => call('DELETE', '/kv/' + enc(key)),
122
+ incr: (key, by) => call('POST', '/kv/' + enc(key) + '/incr', { by: by == null ? 1 : by }),
123
+ list: (opts) => call('GET', '/kv' + qs(opts)),
124
+ };
25
125
  }
26
126
  }
27
127
 
128
+ // Expose BOTH the class (for \`new SwarmSDK()\`) AND a ready-to-use singleton
129
+ // on \`window.swarmSdk\` so pages can call e.g. \`window.swarmSdk.agents.list()\`
130
+ // directly without instantiating.
28
131
  window.SwarmSDK = SwarmSDK;
132
+ window.swarmSdk = new SwarmSDK();
133
+ `;
134
+
135
+ // ─── UI primitives ──────────────────────────────────────────────────────────
136
+ //
137
+ // Auto-injected alongside the SDK. Exposes a tiny set of declarative web
138
+ // components agents can drop into HTML pages without bundling anything. v1:
139
+ // only \`<swarm-diff>\` (unified-diff renderer) + \`<swarm-diff-jumps>\` (a
140
+ // sibling-anchor jump list). All zero-dep, pure DOM — Tailwind utility
141
+ // classes are used freely since the Play CDN is already loaded by
142
+ // PAGE_HEAD_DEFAULTS, but every visual aspect has inline-style fallbacks so
143
+ // the component is still legible if Tailwind fails to load.
144
+
145
+ /**
146
+ * Renders a unified diff as a two-column-gutter HTML table inside a
147
+ * `<swarm-diff>` custom element. Reads `file`, `base-sha`, `head-sha`
148
+ * attributes and parses the element's text content as JSON of shape
149
+ * `{ hunks: [{ old_start, old_lines, new_start, new_lines, lines:
150
+ * [{ type: 'context' | 'add' | 'del', text }], annotations?: [{ line,
151
+ * severity, text }] }] }`. Severity ∈ `error|warn|info`. Each hunk gets a
152
+ * deterministic anchor id so deep-linking + the sibling `<swarm-diff-jumps>`
153
+ * component works.
154
+ *
155
+ * Pure JS, no deps. Tailwind utility classes are sprinkled in but every
156
+ * critical visual property has an inline-style fallback.
157
+ */
158
+ export const SWARM_UI_JS = `
159
+ (function() {
160
+ if (typeof window === 'undefined' || !window.customElements) return;
161
+ if (window.customElements.get('swarm-diff')) return;
162
+
163
+ var SEV_COLOR = {
164
+ error: '#ef4444',
165
+ warn: '#f59e0b',
166
+ info: '#3b82f6',
167
+ };
168
+
169
+ function esc(s) {
170
+ return String(s == null ? '' : s)
171
+ .replace(/&/g, '&amp;')
172
+ .replace(/</g, '&lt;')
173
+ .replace(/>/g, '&gt;')
174
+ .replace(/"/g, '&quot;')
175
+ .replace(/'/g, '&#39;');
176
+ }
177
+
178
+ function slugifyAttr(s) {
179
+ return String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
180
+ }
181
+
182
+ function parseHunks(jsonText) {
183
+ var trimmed = (jsonText || '').trim();
184
+ if (!trimmed) return [];
185
+ try {
186
+ var parsed = JSON.parse(trimmed);
187
+ if (parsed && Array.isArray(parsed.hunks)) return parsed.hunks;
188
+ if (Array.isArray(parsed)) return parsed;
189
+ return [];
190
+ } catch (e) {
191
+ console.warn('[swarm-diff] failed to parse JSON body:', e);
192
+ return [];
193
+ }
194
+ }
195
+
196
+ function renderAnnotation(ann) {
197
+ var color = SEV_COLOR[ann && ann.severity] || SEV_COLOR.info;
198
+ return (
199
+ '<span class="swarm-diff-annot no-print" '
200
+ + 'style="display:inline-block;margin-left:8px;padding:1px 6px;'
201
+ + 'border-radius:4px;font-size:11px;font-weight:600;'
202
+ + 'background:' + color + '22;color:' + color + ';border:1px solid ' + color + '55;">'
203
+ + esc((ann && ann.severity ? ann.severity.toUpperCase() : 'INFO')) + ' · ' + esc(ann && ann.text || '')
204
+ + '</span>'
205
+ );
206
+ }
207
+
208
+ function renderHunk(hunk, hunkIdx, file) {
209
+ var oldLines = hunk.old_lines || 0;
210
+ var newLines = hunk.new_lines || 0;
211
+ var oldStart = hunk.old_start || 0;
212
+ var newStart = hunk.new_start || 0;
213
+ var lines = Array.isArray(hunk.lines) ? hunk.lines : [];
214
+ var annotations = Array.isArray(hunk.annotations) ? hunk.annotations : [];
215
+ // Index annotations by new-side line number for fast lookup per row.
216
+ var annByLine = {};
217
+ for (var i = 0; i < annotations.length; i++) {
218
+ var a = annotations[i];
219
+ if (a && typeof a.line === 'number') {
220
+ if (!annByLine[a.line]) annByLine[a.line] = [];
221
+ annByLine[a.line].push(a);
222
+ }
223
+ }
224
+
225
+ var rowsHtml = '';
226
+ var oldN = oldStart;
227
+ var newN = newStart;
228
+ for (var j = 0; j < lines.length; j++) {
229
+ var line = lines[j] || {};
230
+ var type = line.type || 'context';
231
+ var text = line.text == null ? '' : line.text;
232
+ var bg, oldCell, newCell, sign;
233
+ if (type === 'add') {
234
+ bg = 'rgba(34,197,94,0.10)';
235
+ oldCell = '';
236
+ newCell = String(newN++);
237
+ sign = '+';
238
+ } else if (type === 'del') {
239
+ bg = 'rgba(239,68,68,0.10)';
240
+ oldCell = String(oldN++);
241
+ newCell = '';
242
+ sign = '-';
243
+ } else {
244
+ bg = 'transparent';
245
+ oldCell = String(oldN++);
246
+ newCell = String(newN++);
247
+ sign = ' ';
248
+ }
249
+
250
+ var annHtml = '';
251
+ var anns = annByLine[Number(newCell)] || annByLine[Number(oldCell)] || [];
252
+ for (var k = 0; k < anns.length; k++) annHtml += renderAnnotation(anns[k]);
253
+
254
+ rowsHtml += (
255
+ '<tr style="background:' + bg + ';">'
256
+ + '<td class="swarm-diff-gutter" style="user-select:none;text-align:right;padding:0 8px;color:#7c8aa6;font-size:12px;width:48px;">' + esc(oldCell) + '</td>'
257
+ + '<td class="swarm-diff-gutter" style="user-select:none;text-align:right;padding:0 8px;color:#7c8aa6;font-size:12px;width:48px;">' + esc(newCell) + '</td>'
258
+ + '<td class="swarm-diff-sign" style="user-select:none;text-align:center;padding:0 4px;color:#7c8aa6;font-size:12px;width:18px;">' + esc(sign) + '</td>'
259
+ + '<td class="swarm-diff-code" style="padding:0 8px;white-space:pre-wrap;word-break:break-word;font-family:\\'Space Mono\\',ui-monospace,monospace;font-size:12px;">' + esc(text) + annHtml + '</td>'
260
+ + '</tr>'
261
+ );
262
+ }
263
+
264
+ var anchorSlug = slugifyAttr((file || 'hunk') + '-' + (oldStart || hunkIdx + 1));
265
+ var anchorId = 'swarm-diff-' + anchorSlug;
266
+ var header = (
267
+ '@@ -' + oldStart + ',' + oldLines + ' +' + newStart + ',' + newLines + ' @@'
268
+ );
269
+
270
+ return (
271
+ '<a id="' + esc(anchorId) + '" class="swarm-diff-anchor" data-hunk="' + esc(anchorSlug) + '"></a>'
272
+ + '<div class="swarm-diff-hunk-header" style="padding:6px 12px;background:rgba(124,138,166,0.10);color:#7c8aa6;font-family:\\'Space Mono\\',ui-monospace,monospace;font-size:11px;border-top:1px solid var(--swarm-border,#22304a);">'
273
+ + esc(header)
274
+ + '</div>'
275
+ + '<table class="swarm-diff-table" style="width:100%;border-collapse:collapse;table-layout:fixed;">'
276
+ + '<tbody>' + rowsHtml + '</tbody>'
277
+ + '</table>'
278
+ );
279
+ }
280
+
281
+ function renderDiff(rootEl, diffData) {
282
+ var hunks = (diffData && Array.isArray(diffData.hunks)) ? diffData.hunks : (Array.isArray(diffData) ? diffData : []);
283
+ var file = rootEl.getAttribute('file') || '';
284
+ var baseSha = rootEl.getAttribute('base-sha') || '';
285
+ var headSha = rootEl.getAttribute('head-sha') || '';
286
+
287
+ var shaLine = '';
288
+ if (baseSha || headSha) {
289
+ shaLine = '<span class="swarm-diff-sha" style="font-family:\\'Space Mono\\',ui-monospace,monospace;font-size:11px;color:#7c8aa6;">'
290
+ + esc(baseSha) + ' → ' + esc(headSha)
291
+ + '</span>';
292
+ }
293
+
294
+ var hunksHtml = '';
295
+ for (var i = 0; i < hunks.length; i++) {
296
+ hunksHtml += renderHunk(hunks[i] || {}, i, file);
297
+ }
298
+
299
+ rootEl.innerHTML = (
300
+ '<div class="swarm-diff-root" style="border:1px solid var(--swarm-border,#22304a);border-radius:8px;background:var(--swarm-card,#121826);overflow:hidden;margin:12px 0;break-inside:avoid;">'
301
+ + '<div class="swarm-diff-header" style="display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:rgba(59,130,246,0.10);border-bottom:1px solid var(--swarm-border,#22304a);">'
302
+ + '<span class="swarm-diff-file" style="font-family:\\'Space Mono\\',ui-monospace,monospace;font-size:13px;font-weight:700;color:var(--swarm-text,#e6eaf2);">' + esc(file || '(untitled)') + '</span>'
303
+ + shaLine
304
+ + '</div>'
305
+ + hunksHtml
306
+ + '</div>'
307
+ );
308
+ }
309
+
310
+ // Public function form lives on window.swarmUi so callers can render an
311
+ // arbitrary root element programmatically. The custom element below is just
312
+ // a declarative wrapper around the same render function.
313
+ window.swarmUi = window.swarmUi || {};
314
+ window.swarmUi.renderDiff = renderDiff;
315
+
316
+ // Defer the parse-and-render so the HTML parser has time to finish
317
+ // appending JSON text children. \`connectedCallback\` fires on the opening
318
+ // tag — \`this.textContent\` is empty until children parse. Without a
319
+ // defer, every declarative <swarm-diff> renders an empty header and the
320
+ // JSON text remains visible as orphan children.
321
+ //
322
+ // queueMicrotask alone is NOT enough — Chrome's streaming parser drains
323
+ // microtasks between chunks, so the microtask can run BEFORE the JSON
324
+ // child is appended. We need to wait for the parser to finish the current
325
+ // document load, then read textContent.
326
+ //
327
+ // * \`document.readyState === 'loading'\` ⇒ parser still streaming →
328
+ // wait for DOMContentLoaded (fires after all children are parsed).
329
+ // * otherwise (element was created/inserted dynamically post-load) ⇒
330
+ // queueMicrotask is fine — DOM is stable, just give the caller a tick.
331
+ //
332
+ // Re-entrancy (element moved/reconnected) re-fires connectedCallback so
333
+ // we re-render against current textContent.
334
+ class SwarmDiffElement extends HTMLElement {
335
+ connectedCallback() {
336
+ var self = this;
337
+ var doRender = function() {
338
+ if (!self.isConnected) return;
339
+ var raw = self.textContent || '';
340
+ renderDiff(self, { hunks: parseHunks(raw) });
341
+ // Notify <swarm-diff-jumps> instances so they can pick up new anchors.
342
+ self.dispatchEvent(new CustomEvent('swarm-diff:rendered', { bubbles: true }));
343
+ };
344
+ if (typeof document !== 'undefined' && document.readyState === 'loading') {
345
+ document.addEventListener('DOMContentLoaded', doRender, { once: true });
346
+ } else {
347
+ queueMicrotask(doRender);
348
+ }
349
+ }
350
+ }
351
+ window.customElements.define('swarm-diff', SwarmDiffElement);
352
+
353
+ // Sibling-anchor jump list. Walks subsequent siblings, finds every
354
+ // <swarm-diff data-hunk=...> anchor, and renders a small list of links.
355
+ //
356
+ // Same parse-order hazard as <swarm-diff>: <swarm-diff-jumps> usually
357
+ // appears in the document BEFORE the <swarm-diff> elements it indexes, so
358
+ // we also defer to a microtask AND re-render whenever a <swarm-diff> in the
359
+ // document finishes rendering its anchors.
360
+ class SwarmDiffJumpsElement extends HTMLElement {
361
+ connectedCallback() {
362
+ var self = this;
363
+ var renderJumps = function() {
364
+ var anchors = document.querySelectorAll('.swarm-diff-anchor[data-hunk]');
365
+ if (!anchors.length) {
366
+ self.innerHTML = '<span class="no-print" style="color:#7c8aa6;font-size:12px;">No hunks yet.</span>';
367
+ return;
368
+ }
369
+ var items = '';
370
+ for (var i = 0; i < anchors.length; i++) {
371
+ var a = anchors[i];
372
+ var slug = a.getAttribute('data-hunk') || ('hunk-' + i);
373
+ // Hunk title = nearest preceding diff's file attribute if available.
374
+ var diff = a.closest && a.closest('swarm-diff');
375
+ var file = (diff && diff.getAttribute('file')) || slug;
376
+ items += '<li style="margin:0;padding:2px 0;"><a href="#' + esc(a.id) + '" style="color:#3b82f6;text-decoration:none;font-family:\\'Space Mono\\',ui-monospace,monospace;font-size:12px;">' + esc(file) + '</a></li>';
377
+ }
378
+ self.innerHTML = (
379
+ '<nav class="swarm-diff-jumps no-print" style="padding:8px 12px;border:1px dashed var(--swarm-border,#22304a);border-radius:8px;background:rgba(124,138,166,0.05);">'
380
+ + '<div style="font-size:10px;text-transform:uppercase;letter-spacing:0.08em;color:#7c8aa6;margin-bottom:4px;">Jump to</div>'
381
+ + '<ul style="list-style:none;padding:0;margin:0;">' + items + '</ul>'
382
+ + '</nav>'
383
+ );
384
+ };
385
+ // Wait for the parser to finish initial load before first query —
386
+ // <swarm-diff> elements also defer to DOMContentLoaded so we must
387
+ // run AFTER they finish rendering their anchors. The event listener
388
+ // below handles the live-update case.
389
+ if (typeof document !== 'undefined' && document.readyState === 'loading') {
390
+ document.addEventListener('DOMContentLoaded', function() { queueMicrotask(renderJumps); }, { once: true });
391
+ } else {
392
+ queueMicrotask(renderJumps);
393
+ }
394
+ // Re-render whenever a sibling <swarm-diff> finishes its async render.
395
+ self._onDiffRendered = function() { renderJumps(); };
396
+ document.addEventListener('swarm-diff:rendered', self._onDiffRendered);
397
+ }
398
+ disconnectedCallback() {
399
+ if (this._onDiffRendered) {
400
+ document.removeEventListener('swarm-diff:rendered', this._onDiffRendered);
401
+ }
402
+ }
403
+ }
404
+ window.customElements.define('swarm-diff-jumps', SwarmDiffJumpsElement);
405
+ })();
29
406
  `;