loopengine 0.1.95 → 0.1.98

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 (68) hide show
  1. package/README.md +21 -2
  2. package/dist/bin/ability-manager.d.ts +4 -0
  3. package/dist/bin/ability-manager.js +34 -2
  4. package/dist/bin/cli.d.ts +1 -1
  5. package/dist/bin/cli.js +3 -0
  6. package/dist/core/ability-details.d.ts +31 -0
  7. package/dist/core/ability-details.js +140 -0
  8. package/dist/core/ability-sources.d.ts +59 -0
  9. package/dist/core/ability-sources.js +312 -0
  10. package/dist/core/agent-config.d.ts +21 -1
  11. package/dist/core/discover-agents.js +11 -1
  12. package/dist/core/http-notifier.d.ts +7 -1
  13. package/dist/core/http-notifier.js +68 -9
  14. package/dist/core/http-notify-triggers/email.d.ts +2 -2
  15. package/dist/core/http-notify-triggers/email.js +2 -4
  16. package/dist/core/http-notify-triggers/lark.d.ts +2 -2
  17. package/dist/core/http-notify-triggers/lark.js +2 -4
  18. package/dist/core/http-notify-triggers/slack.d.ts +2 -2
  19. package/dist/core/http-notify-triggers/slack.js +2 -4
  20. package/dist/core/http-notify-triggers/telegram.d.ts +28 -0
  21. package/dist/core/http-notify-triggers/telegram.js +71 -0
  22. package/dist/core/http-notify-triggers/webhook.d.ts +2 -2
  23. package/dist/core/http-notify-triggers/webhook.js +2 -4
  24. package/dist/core/model-calls/meta-model-call.d.ts +15 -0
  25. package/dist/core/model-calls/meta-model-call.js +48 -0
  26. package/dist/core/model-calls/openai-model-call.d.ts +5 -0
  27. package/dist/core/model-calls/openai-model-call.js +47 -1
  28. package/dist/core/model-calls/xai-model-call.d.ts +15 -0
  29. package/dist/core/model-calls/xai-model-call.js +45 -0
  30. package/dist/core/notifier-file.d.ts +40 -6
  31. package/dist/core/notifier-file.js +119 -25
  32. package/dist/core/system-agents/workflow-planner.d.ts +100 -0
  33. package/dist/core/system-agents/workflow-planner.js +275 -0
  34. package/dist/core/workflows.d.ts +26 -0
  35. package/dist/core/workflows.js +39 -0
  36. package/dist/core/workspace-abilities.d.ts +31 -0
  37. package/dist/core/workspace-abilities.js +110 -0
  38. package/dist/index.d.ts +18 -4
  39. package/dist/index.js +17 -3
  40. package/dist/web/ability-details-admin.d.ts +17 -0
  41. package/dist/web/ability-details-admin.js +45 -0
  42. package/dist/web/ability-sources-admin.d.ts +33 -0
  43. package/dist/web/ability-sources-admin.js +65 -0
  44. package/dist/web/agent-file-admin.d.ts +1 -1
  45. package/dist/web/agent-file-admin.js +3 -0
  46. package/dist/web/agents-config-page.js +600 -220
  47. package/dist/web/agents-list-page.js +49 -20
  48. package/dist/web/console-shell.js +244 -3
  49. package/dist/web/dev-ui-styles.js +12 -0
  50. package/dist/web/global-config-page.js +348 -95
  51. package/dist/web/global-config.d.ts +44 -1
  52. package/dist/web/global-config.js +162 -2
  53. package/dist/web/interview-admin.js +2 -2
  54. package/dist/web/messaging-icons.d.ts +2 -0
  55. package/dist/web/messaging-icons.js +7 -0
  56. package/dist/web/notifier-admin.d.ts +33 -15
  57. package/dist/web/notifier-admin.js +109 -65
  58. package/dist/web/provider-icons.d.ts +2 -0
  59. package/dist/web/provider-icons.js +10 -0
  60. package/dist/web/templates-admin.d.ts +3 -0
  61. package/dist/web/templates-admin.js +5 -1
  62. package/dist/web/workflow-admin.d.ts +124 -0
  63. package/dist/web/workflow-admin.js +308 -0
  64. package/dist/web/workflow-page.d.ts +1 -0
  65. package/dist/web/workflow-page.js +226 -0
  66. package/dist/web/workspace-abilities-admin.d.ts +24 -0
  67. package/dist/web/workspace-abilities-admin.js +118 -0
  68. package/package.json +13 -1
@@ -61,22 +61,24 @@ ${consoleSidebarHtml}
61
61
  <label>Name <span class="hint">(lowercase, hyphens allowed: becomes agents/&lt;name&gt;/)</span>
62
62
  <input type="text" name="name" placeholder="weather-agent" pattern="[a-z0-9]+(-[a-z0-9]+)*" required>
63
63
  </label>
64
- <details>
65
- <summary>Prompt and model (optional)</summary>
66
- <label>System prompt
67
- <textarea name="systemPrompt" placeholder="You are ..."></textarea>
68
- </label>
69
- <label>Model provider
70
- <select name="provider">
71
- <option value="anthropic">anthropic</option>
72
- <option value="openai">openai</option>
73
- <option value="deepseek">deepseek</option>
74
- </select>
75
- </label>
76
- <label>Model name <span class="hint">(required for openai/deepseek; defaults to claude-sonnet-5 for anthropic)</span>
77
- <input type="text" name="modelName" placeholder="claude-sonnet-5">
78
- </label>
79
- </details>
64
+ <label>System prompt
65
+ <textarea name="systemPrompt" placeholder="You are ..."></textarea>
66
+ </label>
67
+ <label>Model provider
68
+ <select name="provider">
69
+ <option value="openai" selected>openai</option>
70
+ <option value="anthropic">anthropic</option>
71
+ <option value="gemini">gemini</option>
72
+ <option value="meta">meta</option>
73
+ <option value="xai">xai</option>
74
+ <option value="deepseek">deepseek</option>
75
+ <option value="kimi">kimi</option>
76
+ <option value="glm">glm</option>
77
+ </select>
78
+ </label>
79
+ <label id="modelNameLabel">Model name <span class="hint">(choose Other&hellip; to type a model that isn't listed)</span>
80
+ <input type="text" name="modelName" value="gpt-6-luna" placeholder="model name">
81
+ </label>
80
82
  <div class="row">
81
83
  <button type="submit">Create agent</button>
82
84
  <button type="button" id="cancelNewAgentBtn">Cancel</button>
@@ -152,11 +154,40 @@ ${consoleShellScript}
152
154
  // same as before this existed; loopengine dev picks it up
153
155
  // automatically via its own --include 'agents/*/index.ts' (see
154
156
  // bin/cli.ts's own comment there).
157
+ // Only providers with a key saved can be chosen (leShell.applyProviderKeys).
158
+ // With no key at all, the agent is created without a model (the
159
+ // scaffold's default), so the provider and model fields are switched off
160
+ // and the note says so. If the keys can't be read, nothing is disabled.
161
+ var providerSelect = newAgentForm.querySelector('select[name="provider"]');
162
+ var modelNameLabel = document.getElementById('modelNameLabel');
163
+ var providerKeys = null;
164
+
165
+ function applyProviderKeys() {
166
+ if (!providerKeys) return;
167
+ var any = leShell.applyProviderKeys(newAgentForm, providerKeys);
168
+ providerSelect.disabled = !any;
169
+ modelNameLabel.style.display = any ? '' : 'none';
170
+ }
171
+
172
+ // form.reset() after an agent is created goes back to the markup's
173
+ // openai default even without a key; apply the keys again first. The
174
+ // model dropdown's own reset listener is added later, so runs after.
175
+ newAgentForm.addEventListener('reset', function () { setTimeout(applyProviderKeys, 0); });
176
+
155
177
  newAgentBtn.addEventListener('click', function () {
156
178
  newAgentBtn.style.display = 'none';
157
179
  newAgentForm.style.display = 'grid';
158
180
  newAgentResult.style.display = 'none';
159
181
  newAgentForm.querySelector('input[name="name"]').focus();
182
+ // Checks which providers have keys, then loads the chosen provider's
183
+ // models; both when the form opens, not on every visit to this page.
184
+ // Starts on openai / gpt-6-luna (the markup's defaults) when openai
185
+ // has a key.
186
+ leShell.loadProviderKeys().then(function (keys) {
187
+ providerKeys = keys;
188
+ applyProviderKeys();
189
+ if (!providerSelect.disabled) leShell.wireModelPicker(newAgentForm);
190
+ });
160
191
  });
161
192
 
162
193
  cancelNewAgentBtn.addEventListener('click', function () {
@@ -172,11 +203,9 @@ ${consoleShellScript}
172
203
  var submitBtn = newAgentForm.querySelector('button[type="submit"]');
173
204
  submitBtn.disabled = true;
174
205
 
175
- // systemPrompt/model are both optional — see
206
+ // A blank system prompt or model name is left out of the body — see
176
207
  // adapters/http.ts's parseAgentTemplateOptions for the defaults
177
- // scaffoldAgent falls back to when either is left out of the body
178
- // entirely (blank/untouched Advanced fields are treated the same
179
- // as not sending them at all).
208
+ // scaffoldAgent falls back to then.
180
209
  var body = { name: data.get('name') };
181
210
  var systemPrompt = data.get('systemPrompt');
182
211
  if (systemPrompt && systemPrompt.trim()) body.systemPrompt = systemPrompt;
@@ -12,6 +12,11 @@
12
12
  // leShell.renderHead(opts) agent header + tabs HTML (see below)
13
13
  // leShell.setTab(tab) move the tab highlight without a reload
14
14
  // leShell.esc(s) HTML-escape
15
+ // leShell.wireModelPicker(form) model dropdown for a provider select
16
+ // leShell.loadProviderKeys() Promise of {provider: key saved?}, or null
17
+ // leShell.applyProviderKeys(form, keys, keep) only providers with a key
18
+ // leShell.abilityOriginBadge(origin) Public/Private badge for an ability
19
+ // leShell.toggleAbilityDetails(btn) an ability's tools, skills and settings
15
20
  // Same embedded-string conventions as the pages themselves: plain
16
21
  // string concatenation and no backslashes inside the script, so nothing
17
22
  // here needs double-escaping in the template literal.
@@ -24,6 +29,7 @@ export const consoleSidebarHtml = `<aside class="le-sidebar" aria-label="Console
24
29
  <ul class="le-agent-list" id="leAgentList"></ul>
25
30
  <a class="le-find" href="/interview" data-le-nav="interview">Find your first agent</a>
26
31
  <a class="le-find" href="/templates" data-le-nav="templates">Start from a template</a>
32
+ <a class="le-find" href="/workflows/new" data-le-nav="workflows">Describe a workflow</a>
27
33
  <nav class="le-foot" aria-label="Settings">
28
34
  <a href="/config?section=models" data-le-nav="models">Models</a>
29
35
  <a href="/config?section=abilities" data-le-nav="abilities">Abilities</a>
@@ -44,9 +50,12 @@ export const consoleShellScript = `<script>
44
50
  // that's certainly missing (see core/agent-env.ts). Filled in per agent
45
51
  // after the list loads.
46
52
  var toSet = {};
53
+ // Tools and Skills live under Advanced (an agent's own tools and skills,
54
+ // Composio, subagents): what an agent can do is shown per ability in
55
+ // Abilities. ?tab=tools and ?tab=skills still open Advanced.
47
56
  var TABS = [
48
- ['overview', 'Overview'], ['chat', 'Chat'], ['abilities', 'Abilities'], ['tools', 'Tools'],
49
- ['skills', 'Skills'], ['actauth', 'Permissions'], ['messaging', 'Messaging'], ['schedules', 'Schedules'], ['env', 'Environment']
57
+ ['overview', 'Overview'], ['chat', 'Chat'], ['abilities', 'Abilities'], ['actauth', 'Permissions'],
58
+ ['messaging', 'Messaging'], ['schedules', 'Schedules'], ['env', 'Environment'], ['advanced', 'Advanced']
50
59
  ];
51
60
 
52
61
  function esc(s) {
@@ -193,6 +202,238 @@ export const consoleShellScript = `<script>
193
202
  if (app.classList.contains('nav-open') && !ev.target.closest('.le-sidebar')) app.classList.remove('nav-open');
194
203
  });
195
204
 
196
- window.leShell = { loadError: function () { return loadError; }, ready: ready, renderHead: renderHead, setTab: setTab, setActive: setActive, esc: esc, tabHref: tabHref, envCountHtml: envCountHtml, setCount: setCount };
205
+ // A model dropdown for a form with select[name=provider] and
206
+ // input[name=modelName] (New agent, and an agent's Model form): the
207
+ // provider's models from GET /config/models/:provider/models, reloaded
208
+ // when the provider changes. The text input is still what the form
209
+ // submits; it shows for "Other", which is all the dropdown offers when
210
+ // a provider has no models to list. A short or empty list says why
211
+ // (no key saved, provider down) under it. Safe to call more than once
212
+ // for the same form.
213
+ var OTHER_MODEL = '__other__';
214
+ function wireModelPicker(form) {
215
+ var providerSelect = form.querySelector('select[name="provider"]');
216
+ var input = form.querySelector('input[name="modelName"]');
217
+ if (!providerSelect || !input || input.getAttribute('data-le-model-picker')) return;
218
+ input.setAttribute('data-le-model-picker', '1');
219
+ var select = document.createElement('select');
220
+ select.setAttribute('aria-label', 'Model name');
221
+ var note = document.createElement('span');
222
+ note.className = 'hint';
223
+ note.style.display = 'none';
224
+ input.parentNode.insertBefore(select, input);
225
+ input.parentNode.insertBefore(note, input.nextSibling);
226
+ var request = 0;
227
+
228
+ function showInput(show) { input.style.display = show ? '' : 'none'; }
229
+ function showNote(message) {
230
+ note.textContent = message || '';
231
+ note.style.display = message ? '' : 'none';
232
+ }
233
+
234
+ select.addEventListener('change', function () {
235
+ if (select.value === OTHER_MODEL) {
236
+ input.value = '';
237
+ showInput(true);
238
+ input.focus();
239
+ } else {
240
+ input.value = select.value;
241
+ showInput(false);
242
+ }
243
+ });
244
+
245
+ function load() {
246
+ var provider = providerSelect.value;
247
+ var current = input.value.trim();
248
+ var mine = ++request;
249
+ select.innerHTML = '<option>Loading models&hellip;</option>';
250
+ select.disabled = true;
251
+ showNote('');
252
+ showInput(false);
253
+ fetch('/config/models/' + encodeURIComponent(provider) + '/models')
254
+ .then(function (r) { return r.json(); })
255
+ .then(function (data) {
256
+ if (mine !== request) return;
257
+ var models = data.models || [];
258
+ showNote(data.error);
259
+ // Only Anthropic has a default model (see agents' model config).
260
+ var options = provider === 'anthropic' ? '<option value="">(provider default)</option>' : '';
261
+ if (current && models.indexOf(current) === -1) options += '<option value="' + esc(current) + '">' + esc(current) + '</option>';
262
+ options += models.map(function (m) { return '<option value="' + esc(m) + '">' + esc(m) + '</option>'; }).join('');
263
+ options += '<option value="' + OTHER_MODEL + '">Other&hellip;</option>';
264
+ select.innerHTML = options;
265
+ select.disabled = false;
266
+ if (current || provider === 'anthropic' || models.length) {
267
+ select.value = current || (provider === 'anthropic' ? '' : models[0]);
268
+ input.value = select.value;
269
+ } else {
270
+ // Nothing listed: "Other" with the name box open.
271
+ select.value = OTHER_MODEL;
272
+ showInput(true);
273
+ }
274
+ })
275
+ .catch(function (err) {
276
+ if (mine !== request) return;
277
+ select.innerHTML = '<option value="' + OTHER_MODEL + '">Other&hellip;</option>';
278
+ select.disabled = false;
279
+ showInput(true);
280
+ showNote('Could not list models: ' + err.message);
281
+ });
282
+ }
283
+
284
+ providerSelect.addEventListener('change', function () { input.value = ''; load(); });
285
+ // form.reset() (after New agent creates one) puts the provider and
286
+ // the hidden model name back to their defaults, but not this
287
+ // dropdown's options; reload once the reset has happened.
288
+ form.addEventListener('reset', function () { setTimeout(load, 0); });
289
+ load();
290
+ }
291
+
292
+ // Which model providers have a key saved, from GET /config/models:
293
+ // {provider: true/false}, or null when that can't be read.
294
+ function loadProviderKeys() {
295
+ return fetch('/config/models')
296
+ .then(function (r) { return r.ok ? r.json() : null; })
297
+ .then(function (data) {
298
+ if (!data) return null;
299
+ var keys = {};
300
+ data.providers.forEach(function (p) { keys[p.provider] = p.configured; });
301
+ return keys;
302
+ })
303
+ .catch(function () { return null; });
304
+ }
305
+
306
+ // Lets a form's select[name=provider] choose only providers with a key
307
+ // saved, since an agent on one without a key can't answer. The rest stay
308
+ // listed, disabled and marked "(no key)", with a note under the select
309
+ // linking to the Models page. keepProvider (an agent's current provider)
310
+ // stays selectable without a key, so its other settings can still be
311
+ // saved. Moves a disabled choice to the first provider with a key and
312
+ // fires change, so the model dropdown follows. Returns that first
313
+ // provider with a key, or null when there's none.
314
+ function applyProviderKeys(form, keys, keepProvider) {
315
+ var select = form.querySelector('select[name="provider"]');
316
+ if (!select || !keys) return null;
317
+ var note = form.querySelector('[data-le-provider-note]');
318
+ if (!note) {
319
+ note = document.createElement('p');
320
+ note.className = 'hint';
321
+ note.style.margin = '0';
322
+ note.setAttribute('data-le-provider-note', '');
323
+ var label = select.closest('label') || select;
324
+ label.parentNode.insertBefore(note, label.nextSibling);
325
+ }
326
+ var firstWithKey = null;
327
+ var withoutKey = 0;
328
+ Array.prototype.forEach.call(select.options, function (option) {
329
+ var hasKey = Boolean(keys[option.value]);
330
+ option.disabled = !hasKey && option.value !== keepProvider;
331
+ option.textContent = hasKey ? option.value : option.value + ' (no key)';
332
+ if (hasKey && !firstWithKey) firstWithKey = option.value;
333
+ if (!hasKey) withoutKey++;
334
+ });
335
+ var modelsLink = '<a href="/config?section=models">Models page</a>';
336
+ note.innerHTML = firstWithKey
337
+ ? 'Add a key on the ' + modelsLink + ' to use more providers.'
338
+ : 'No model provider has a key yet. Add one on the ' + modelsLink + ' to choose a model.';
339
+ note.style.display = withoutKey ? '' : 'none';
340
+ var chosen = select.options[select.selectedIndex];
341
+ if (firstWithKey && chosen && chosen.disabled) {
342
+ select.value = firstWithKey;
343
+ var input = form.querySelector('input[name="modelName"]');
344
+ if (input) input.value = '';
345
+ select.dispatchEvent(new Event('change'));
346
+ }
347
+ return firstWithKey;
348
+ }
349
+
350
+ // ---- Abilities: where one came from, and what it can do ----
351
+ // Used by the Abilities page and each agent's Abilities tab.
352
+
353
+ var DECISION_LABELS = { allow: 'Allow', ask: 'Ask me', deny: 'Never' };
354
+
355
+ function decisionPill(decision) {
356
+ return decision ? '<span class="le-pill ' + decision + '">' + DECISION_LABELS[decision] + '</span>' : '<span class="hint">No rule</span>';
357
+ }
358
+
359
+ // Public (npm) or Private, with where it came from on hover.
360
+ function abilityOriginBadge(origin) {
361
+ if (!origin) return '';
362
+ return '<span class="le-pill ' + (origin.kind === 'private' ? 'accent' : '') + '" title="' + esc(origin.label) + '">' +
363
+ (origin.kind === 'private' ? 'Private' : 'Public') + '</span> <span class="hint">' + esc(origin.label) + '</span>';
364
+ }
365
+
366
+
367
+
368
+
369
+
370
+ // What an ability can do: its tools (what each one does and its
371
+ // permission: this agent's own when it's installed, else the one it
372
+ // ships with), skills and settings.
373
+ function renderAbilityDetails(d) {
374
+ var tools = d.tools || [];
375
+ var skills = d.skills || [];
376
+ var settings = d.settings || [];
377
+ var toolRows = tools.map(function (t) {
378
+ var permission = d.agent
379
+ ? decisionPill(t.agentDecision) + (t.decision && t.decision !== t.agentDecision ? ' <span class="hint">ships as ' + DECISION_LABELS[t.decision] + '</span>' : '')
380
+ : decisionPill(t.decision);
381
+ return '<tr><td><code>' + esc(t.name) + '</code></td><td>' + (t.description ? esc(t.description) : '<span class="hint">No description</span>') + '</td><td>' + permission + '</td></tr>';
382
+ }).join('');
383
+ // A one-line summary at the top: how many tools, by permission, and skills.
384
+ var counts = {};
385
+ tools.forEach(function (t) {
386
+ var decision = d.agent ? t.agentDecision : t.decision;
387
+ if (decision) counts[decision] = (counts[decision] || 0) + 1;
388
+ });
389
+ var byDecision = ['allow', 'ask', 'deny'].filter(function (k) { return counts[k]; }).map(function (k) { return counts[k] + ' ' + DECISION_LABELS[k]; });
390
+ var summary = tools.length + (tools.length === 1 ? ' tool' : ' tools') + (byDecision.length ? ' (' + byDecision.join(', ') + ')' : '') +
391
+ ' ' + String.fromCharCode(183) + ' ' + skills.length + (skills.length === 1 ? ' skill' : ' skills') +
392
+ (settings.length ? ' ' + String.fromCharCode(183) + ' ' + settings.length + (settings.length === 1 ? ' setting' : ' settings') : '');
393
+ return '<div class="ability-details">' +
394
+ '<div class="ability-details-head"><span class="ability-details-title">What <code>' + esc(d.name) + '</code>' + (d.version ? ' <span class="hint">' + esc(d.version) + '</span>' : '') + ' can do</span>' +
395
+ '<span class="ability-details-summary">' + summary + '</span></div>' +
396
+ (d.description ? '<p style="margin:0 0 6px">' + esc(d.description) + '</p>' : '') +
397
+ '<h4>Tools (' + tools.length + ')</h4>' +
398
+ (tools.length
399
+ ? '<table><thead><tr><th>Tool</th><th>What it does</th><th>' + (d.agent ? 'Permission in ' + esc(d.agent) : 'Permission it ships with') + '</th></tr></thead><tbody>' + toolRows + '</tbody></table>'
400
+ : '<p class="hint">No tools.</p>') +
401
+ '<h4>Skills (' + skills.length + ')</h4>' +
402
+ (skills.length
403
+ ? '<ul class="ability-skills">' + skills.map(function (s) { return '<li><code>' + esc(s.name) + '</code>' + (s.description ? ' ' + String.fromCharCode(8212) + ' ' + esc(s.description) : '') + '</li>'; }).join('') + '</ul>'
404
+ : '<p class="hint">No skills.</p>') +
405
+ (settings.length
406
+ ? '<h4>Settings it needs (' + settings.length + ')</h4><ul class="ability-skills">' + settings.map(function (s) { return '<li><code>' + esc(s.name) + '</code>' + (s.description ? ' ' + String.fromCharCode(8212) + ' ' + esc(s.description) : '') + '</li>'; }).join('') + '</ul>'
407
+ : '') +
408
+ '</div>';
409
+ }
410
+
411
+ // Opens (or closes) an ability's details in a row right under its own.
412
+ function toggleAbilityDetails(btn) {
413
+ var tr = btn.closest('tr');
414
+ var next = tr.nextElementSibling;
415
+ if (next && next.classList.contains('ability-details-row')) {
416
+ next.remove();
417
+ btn.textContent = 'Details';
418
+ btn.classList.remove('is-open');
419
+ return;
420
+ }
421
+ var row = document.createElement('tr');
422
+ row.className = 'ability-details-row';
423
+ row.innerHTML = '<td colspan="' + tr.children.length + '"><div class="ability-details"><p class="hint" style="margin:0">Reading what it can do' + String.fromCharCode(8230) + '</p></div></td>';
424
+ tr.parentNode.insertBefore(row, tr.nextSibling);
425
+ btn.textContent = 'Hide details';
426
+ btn.classList.add('is-open');
427
+ var agent = btn.dataset.agent;
428
+ fetch('/abilities/details?spec=' + encodeURIComponent(btn.dataset.spec) + (agent ? '&agent=' + encodeURIComponent(agent) : ''))
429
+ .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
430
+ .then(function (r) {
431
+ row.firstChild.innerHTML = r.ok ? renderAbilityDetails(r.body) : '<div class="ability-details"><p class="error" style="margin:0">Could not read it: ' + esc(r.body.error || 'unknown error') + '</p></div>';
432
+ })
433
+ .catch(function (err) { row.firstChild.innerHTML = '<div class="ability-details"><p class="error" style="margin:0">Could not read it: ' + esc(err.message) + '</p></div>'; });
434
+ }
435
+
436
+
437
+ window.leShell = { loadError: function () { return loadError; }, ready: ready, renderHead: renderHead, setTab: setTab, setActive: setActive, esc: esc, tabHref: tabHref, envCountHtml: envCountHtml, setCount: setCount, wireModelPicker: wireModelPicker, loadProviderKeys: loadProviderKeys, applyProviderKeys: applyProviderKeys, abilityOriginBadge: abilityOriginBadge, toggleAbilityDetails: toggleAbilityDetails };
197
438
  })();
198
439
  </script>`;
@@ -107,6 +107,18 @@ export const devUiCss = `
107
107
  .le-pill.allow, .le-pill.good { background: var(--good-soft); color: var(--good); }
108
108
  .le-pill.ask, .le-pill.warn { background: var(--warn-soft); color: var(--warn); }
109
109
  .le-pill.deny, .le-pill.bad { background: var(--bad-soft); color: var(--bad); }
110
+ /* An ability's details (leShell.toggleAbilityDetails), on the Abilities
111
+ page and each agent's Abilities tab. */
112
+ .ability-details-row > td { padding: 0 10px 14px; border-top: 0; background: transparent; }
113
+ .ability-details { border: 1px solid var(--accent); border-left-width: 4px; border-radius: var(--radius); background: var(--accent-soft); padding: 12px 16px 14px; }
114
+ .ability-details-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 6px 16px; margin: 0 0 8px; padding-bottom: 8px; border-bottom: 1px solid var(--line); }
115
+ .ability-details-title { font-size: 14px; font-weight: 600; }
116
+ .ability-details-summary { font-size: 12.5px; color: var(--ink-muted); }
117
+ .ability-details h4 { font-size: 13px; font-weight: 600; margin: 12px 0 6px; }
118
+ .ability-details table { width: 100%; background: var(--surface); border-radius: var(--radius); }
119
+ .ability-details-btn.is-open { border-color: var(--accent); color: var(--accent-ink, var(--accent)); }
120
+ .ability-skills { margin: 0; padding-left: 18px; }
121
+ .ability-skills li { margin: 3px 0; }
110
122
  .le-pill.neutral { background: var(--surface-2); color: var(--ink-muted); }
111
123
  .le-pill.accent { background: var(--accent-soft); color: var(--accent-ink); }
112
124