loopengine 0.1.95 → 0.1.97
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 +21 -2
- package/dist/bin/ability-manager.d.ts +4 -0
- package/dist/bin/ability-manager.js +34 -2
- package/dist/bin/cli.d.ts +1 -1
- package/dist/bin/cli.js +3 -0
- package/dist/core/ability-details.d.ts +31 -0
- package/dist/core/ability-details.js +140 -0
- package/dist/core/ability-sources.d.ts +59 -0
- package/dist/core/ability-sources.js +312 -0
- package/dist/core/agent-config.d.ts +21 -1
- package/dist/core/discover-agents.js +11 -1
- package/dist/core/http-notifier.d.ts +7 -1
- package/dist/core/http-notifier.js +68 -9
- package/dist/core/http-notify-triggers/email.d.ts +2 -2
- package/dist/core/http-notify-triggers/email.js +2 -4
- package/dist/core/http-notify-triggers/lark.d.ts +2 -2
- package/dist/core/http-notify-triggers/lark.js +2 -4
- package/dist/core/http-notify-triggers/slack.d.ts +2 -2
- package/dist/core/http-notify-triggers/slack.js +2 -4
- package/dist/core/http-notify-triggers/telegram.d.ts +28 -0
- package/dist/core/http-notify-triggers/telegram.js +71 -0
- package/dist/core/http-notify-triggers/webhook.d.ts +2 -2
- package/dist/core/http-notify-triggers/webhook.js +2 -4
- package/dist/core/model-calls/meta-model-call.d.ts +15 -0
- package/dist/core/model-calls/meta-model-call.js +48 -0
- package/dist/core/model-calls/openai-model-call.d.ts +5 -0
- package/dist/core/model-calls/openai-model-call.js +47 -1
- package/dist/core/model-calls/xai-model-call.d.ts +15 -0
- package/dist/core/model-calls/xai-model-call.js +45 -0
- package/dist/core/notifier-file.d.ts +40 -6
- package/dist/core/notifier-file.js +119 -25
- package/dist/core/system-agents/workflow-planner.d.ts +100 -0
- package/dist/core/system-agents/workflow-planner.js +278 -0
- package/dist/core/workflows.d.ts +26 -0
- package/dist/core/workflows.js +39 -0
- package/dist/core/workspace-abilities.d.ts +31 -0
- package/dist/core/workspace-abilities.js +110 -0
- package/dist/index.d.ts +18 -4
- package/dist/index.js +17 -3
- package/dist/web/ability-details-admin.d.ts +17 -0
- package/dist/web/ability-details-admin.js +45 -0
- package/dist/web/ability-sources-admin.d.ts +33 -0
- package/dist/web/ability-sources-admin.js +65 -0
- package/dist/web/agent-file-admin.d.ts +1 -1
- package/dist/web/agent-file-admin.js +3 -0
- package/dist/web/agents-config-page.js +600 -220
- package/dist/web/agents-list-page.js +49 -20
- package/dist/web/console-shell.js +244 -3
- package/dist/web/dev-ui-styles.js +12 -0
- package/dist/web/global-config-page.js +348 -95
- package/dist/web/global-config.d.ts +44 -1
- package/dist/web/global-config.js +162 -2
- package/dist/web/interview-admin.js +2 -2
- package/dist/web/messaging-icons.d.ts +2 -0
- package/dist/web/messaging-icons.js +7 -0
- package/dist/web/notifier-admin.d.ts +33 -15
- package/dist/web/notifier-admin.js +109 -65
- package/dist/web/provider-icons.d.ts +2 -0
- package/dist/web/provider-icons.js +10 -0
- package/dist/web/templates-admin.d.ts +3 -0
- package/dist/web/templates-admin.js +5 -1
- package/dist/web/workflow-admin.d.ts +124 -0
- package/dist/web/workflow-admin.js +308 -0
- package/dist/web/workflow-page.d.ts +1 -0
- package/dist/web/workflow-page.js +226 -0
- package/dist/web/workspace-abilities-admin.d.ts +24 -0
- package/dist/web/workspace-abilities-admin.js +118 -0
- package/package.json +13 -1
|
@@ -61,22 +61,24 @@ ${consoleSidebarHtml}
|
|
|
61
61
|
<label>Name <span class="hint">(lowercase, hyphens allowed: becomes agents/<name>/)</span>
|
|
62
62
|
<input type="text" name="name" placeholder="weather-agent" pattern="[a-z0-9]+(-[a-z0-9]+)*" required>
|
|
63
63
|
</label>
|
|
64
|
-
<
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
<
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
</
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
</
|
|
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… 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
|
-
//
|
|
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
|
|
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'], ['
|
|
49
|
-
['
|
|
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
|
-
|
|
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…</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…</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…</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
|
|