loopengine 0.1.63 → 0.1.65
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 +20 -11
- package/dist/web/agents-config-page.js +232 -294
- package/dist/web/agents-list-page.js +80 -99
- package/dist/web/console-shell.d.ts +2 -0
- package/dist/web/console-shell.js +165 -0
- package/dist/web/dev-ui-styles.js +146 -40
- package/dist/web/env-admin.js +4 -6
- package/dist/web/global-config-page.js +26 -80
- package/dist/web/playground.js +161 -105
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// that route always returned, unchanged. See web/dev-ui-styles.ts for
|
|
7
7
|
// why the look is a shared import rather than a third copy of the same CSS.
|
|
8
8
|
import { devUiCss } from './dev-ui-styles.js';
|
|
9
|
+
import { consoleShellScript, consoleSidebarHtml } from './console-shell.js';
|
|
9
10
|
export const agentsListPageHtml = `<!doctype html>
|
|
10
11
|
<html lang="en">
|
|
11
12
|
<head>
|
|
@@ -13,104 +14,80 @@ export const agentsListPageHtml = `<!doctype html>
|
|
|
13
14
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
14
15
|
<title>LoopEngine Agents</title>
|
|
15
16
|
<style>${devUiCss}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
justify-content: space-between;
|
|
28
|
-
gap: 12px;
|
|
29
|
-
}
|
|
30
|
-
li .name { font-family: ui-monospace, monospace; font-size: 14px; font-weight: 600; }
|
|
31
|
-
li .prompt {
|
|
32
|
-
font-size: 12px;
|
|
33
|
-
color: light-dark(#666, #999);
|
|
34
|
-
margin-top: 2px;
|
|
35
|
-
max-width: 380px;
|
|
36
|
-
overflow: hidden;
|
|
37
|
-
text-overflow: ellipsis;
|
|
38
|
-
white-space: nowrap;
|
|
39
|
-
}
|
|
40
|
-
li .links { display: flex; gap: 10px; font-size: 12px; flex-shrink: 0; }
|
|
41
|
-
li .links a {
|
|
42
|
-
text-decoration: none;
|
|
43
|
-
border: 1px solid light-dark(#ccc, #444);
|
|
44
|
-
border-radius: 6px;
|
|
45
|
-
padding: 4px 9px;
|
|
46
|
-
}
|
|
47
|
-
li .links a:hover { background: light-dark(#eee, #2a2a2e); }
|
|
48
|
-
#empty { color: light-dark(#666, #999); font-size: 13px; }
|
|
49
|
-
.page-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 4px; }
|
|
50
|
-
.page-head h1 { margin: 0; }
|
|
51
|
-
.hint { font-size: 12px; color: light-dark(#666, #999); }
|
|
52
|
-
.error { font-size: 12px; color: light-dark(#991b1b, #f87171); }
|
|
17
|
+
#agentUl { list-style: none; margin: 0; padding: 0; }
|
|
18
|
+
#agentUl li { display: contents; }
|
|
19
|
+
.agent-card { display: grid; gap: 10px; align-content: start; }
|
|
20
|
+
.agent-card .name { font-weight: 600; font-size: 15px; }
|
|
21
|
+
.agent-card .prompt { font-size: 13px; color: var(--ink-muted); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
|
22
|
+
.agent-card .links { display: flex; gap: 8px; flex-wrap: wrap; margin-top: auto; }
|
|
23
|
+
.agent-card .links a { font-size: 12.5px; font-weight: 500; text-decoration: none; border: 1px solid var(--line); border-radius: 6px; padding: 4px 10px; color: var(--ink); }
|
|
24
|
+
.agent-card .links a.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
|
|
25
|
+
#empty { color: var(--ink-muted); }
|
|
26
|
+
.hint { font-size: 12px; color: var(--ink-muted); }
|
|
27
|
+
.error { font-size: 13px; color: var(--bad); }
|
|
53
28
|
#newAgentForm {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
#newAgentForm .row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
61
|
-
#newAgentForm .row input { flex: 1; min-width: 180px; max-width: 280px; }
|
|
62
|
-
#newAgentForm details { font-size: 12px; }
|
|
63
|
-
#newAgentForm details summary { cursor: pointer; color: light-dark(#666, #999); margin-bottom: 8px; }
|
|
64
|
-
#newAgentForm details label {
|
|
65
|
-
display: flex;
|
|
66
|
-
flex-direction: column;
|
|
67
|
-
gap: 4px;
|
|
68
|
-
margin-bottom: 8px;
|
|
69
|
-
font-size: 12px;
|
|
29
|
+
border: 1px solid var(--line);
|
|
30
|
+
border-radius: var(--radius);
|
|
31
|
+
padding: 16px;
|
|
32
|
+
display: grid;
|
|
33
|
+
gap: 12px;
|
|
34
|
+
max-width: 560px;
|
|
70
35
|
}
|
|
71
|
-
#newAgentForm
|
|
36
|
+
#newAgentForm h2 { font-size: 15px; margin: 0; }
|
|
37
|
+
#newAgentForm label { display: grid; gap: 4px; font-size: 13px; }
|
|
38
|
+
#newAgentForm .row { display: flex; gap: 8px; flex-wrap: wrap; }
|
|
39
|
+
#newAgentForm details summary { cursor: pointer; color: var(--ink-muted); font-size: 13px; }
|
|
40
|
+
#newAgentForm details[open] { display: grid; gap: 10px; }
|
|
41
|
+
#newAgentForm textarea { min-height: 72px; }
|
|
72
42
|
</style>
|
|
73
43
|
</head>
|
|
74
|
-
<body>
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
<
|
|
79
|
-
|
|
80
|
-
<
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
</div>
|
|
85
|
-
<p class="intro">Every AgentConfig discovered under agents/ — open one in the playground to chat with it, or in the config viewer to see its tools, permissions, and hooks.</p>
|
|
86
|
-
<form id="newAgentForm" style="display:none">
|
|
87
|
-
<div class="row">
|
|
88
|
-
<input type="text" name="name" placeholder="weather-agent" pattern="[a-z0-9]+(-[a-z0-9]+)*" required>
|
|
89
|
-
<button type="submit">Create</button>
|
|
90
|
-
<button type="button" id="cancelNewAgentBtn">Cancel</button>
|
|
44
|
+
<body data-le-page="agents">
|
|
45
|
+
<div class="le-app">
|
|
46
|
+
${consoleSidebarHtml}
|
|
47
|
+
<main class="le-main">
|
|
48
|
+
<div class="le-head plain">
|
|
49
|
+
<div class="le-head-row">
|
|
50
|
+
<div class="le-title"><button type="button" class="le-menu-btn" data-le-menu aria-label="Open navigation">☰</button>
|
|
51
|
+
<div><h1>Agents</h1><p>Every agent under agents/ in this project. Open one to chat with it or change its tools, permissions and settings.</p></div>
|
|
52
|
+
</div>
|
|
53
|
+
<button type="button" id="newAgentBtn" class="primary">New agent</button>
|
|
91
54
|
</div>
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<select name="provider">
|
|
99
|
-
<option value="anthropic">anthropic</option>
|
|
100
|
-
<option value="openai">openai</option>
|
|
101
|
-
<option value="deepseek">deepseek</option>
|
|
102
|
-
</select>
|
|
103
|
-
</label>
|
|
104
|
-
<label>Model name <span class="hint">(required for openai/deepseek; defaults to claude-sonnet-5 for anthropic)</span>
|
|
105
|
-
<input type="text" name="modelName" placeholder="claude-sonnet-5">
|
|
55
|
+
</div>
|
|
56
|
+
<div class="le-scroll"><div class="le-page">
|
|
57
|
+
<form id="newAgentForm" style="display:none">
|
|
58
|
+
<h2>New agent</h2>
|
|
59
|
+
<label>Name <span class="hint">(lowercase, hyphens allowed: becomes agents/<name>/)</span>
|
|
60
|
+
<input type="text" name="name" placeholder="weather-agent" pattern="[a-z0-9]+(-[a-z0-9]+)*" required>
|
|
106
61
|
</label>
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
62
|
+
<details>
|
|
63
|
+
<summary>Prompt and model (optional)</summary>
|
|
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="anthropic">anthropic</option>
|
|
70
|
+
<option value="openai">openai</option>
|
|
71
|
+
<option value="deepseek">deepseek</option>
|
|
72
|
+
</select>
|
|
73
|
+
</label>
|
|
74
|
+
<label>Model name <span class="hint">(required for openai/deepseek; defaults to claude-sonnet-5 for anthropic)</span>
|
|
75
|
+
<input type="text" name="modelName" placeholder="claude-sonnet-5">
|
|
76
|
+
</label>
|
|
77
|
+
</details>
|
|
78
|
+
<div class="row">
|
|
79
|
+
<button type="submit">Create agent</button>
|
|
80
|
+
<button type="button" id="cancelNewAgentBtn">Cancel</button>
|
|
81
|
+
</div>
|
|
82
|
+
</form>
|
|
83
|
+
<p class="error" id="newAgentError" style="display:none"></p>
|
|
84
|
+
<p class="hint" id="newAgentResult" style="display:none"></p>
|
|
85
|
+
<ul id="agentUl" class="le-cards"></ul>
|
|
86
|
+
<p id="empty" style="display:none">No agents yet. Create one above, or add a folder under agents/.</p>
|
|
87
|
+
</div></div>
|
|
113
88
|
</main>
|
|
89
|
+
</div>
|
|
90
|
+
${consoleShellScript}
|
|
114
91
|
<script>
|
|
115
92
|
(function () {
|
|
116
93
|
var agentUl = document.getElementById('agentUl');
|
|
@@ -145,12 +122,13 @@ export const agentsListPageHtml = `<!doctype html>
|
|
|
145
122
|
var li = document.createElement('li');
|
|
146
123
|
var qs = '?agent=' + encodeURIComponent(agent.name);
|
|
147
124
|
li.innerHTML =
|
|
148
|
-
'<div
|
|
149
|
-
'<div class="
|
|
125
|
+
'<div class="le-card agent-card">' +
|
|
126
|
+
'<div class="name">' + escapeHtml(agent.name) + '</div>' +
|
|
127
|
+
'<div class="prompt">' + escapeHtml(agent.systemPrompt) + '</div>' +
|
|
150
128
|
'<div class="links">' +
|
|
151
|
-
'<a href="/
|
|
152
|
-
'<a href="/
|
|
153
|
-
'</div>';
|
|
129
|
+
'<a class="primary" href="/playground' + qs + '">Chat</a>' +
|
|
130
|
+
'<a href="/agents/config' + qs + '">Configure</a>' +
|
|
131
|
+
'</div></div>';
|
|
154
132
|
agentUl.appendChild(li);
|
|
155
133
|
}
|
|
156
134
|
})
|
|
@@ -174,7 +152,7 @@ export const agentsListPageHtml = `<!doctype html>
|
|
|
174
152
|
// bin/cli.ts's own comment there).
|
|
175
153
|
newAgentBtn.addEventListener('click', function () {
|
|
176
154
|
newAgentBtn.style.display = 'none';
|
|
177
|
-
newAgentForm.style.display = '
|
|
155
|
+
newAgentForm.style.display = 'grid';
|
|
178
156
|
newAgentResult.style.display = 'none';
|
|
179
157
|
newAgentForm.querySelector('input[name="name"]').focus();
|
|
180
158
|
});
|
|
@@ -220,8 +198,9 @@ export const agentsListPageHtml = `<!doctype html>
|
|
|
220
198
|
newAgentForm.reset();
|
|
221
199
|
newAgentResult.style.display = 'block';
|
|
222
200
|
if (result.body.registered) {
|
|
223
|
-
|
|
224
|
-
|
|
201
|
+
// Straight to the new agent, which also refreshes the sidebar's
|
|
202
|
+
// agent list.
|
|
203
|
+
location.href = '/agents/config?agent=' + encodeURIComponent(body.name);
|
|
225
204
|
} else {
|
|
226
205
|
newAgentResult.textContent =
|
|
227
206
|
'Created ' + result.body.path + ', but it could not be loaded into this running server' +
|
|
@@ -239,6 +218,8 @@ export const agentsListPageHtml = `<!doctype html>
|
|
|
239
218
|
});
|
|
240
219
|
|
|
241
220
|
loadAgents();
|
|
221
|
+
// The sidebar's "+ New" links here with ?new=1.
|
|
222
|
+
if (new URLSearchParams(location.search).get('new')) newAgentBtn.click();
|
|
242
223
|
})();
|
|
243
224
|
</script>
|
|
244
225
|
</body>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// The app shell every console page renders inside: a sidebar listing the
|
|
2
|
+
// agents (plus Models and Integrations), and, for a page about one agent,
|
|
3
|
+
// a header with its name, prompt and tabs. The tabs span two pages —
|
|
4
|
+
// Chat is playground.ts, every other tab is agents-config-page.ts — so
|
|
5
|
+
// both render the header through this one script and read as one app.
|
|
6
|
+
//
|
|
7
|
+
// A page opts in with <body data-le-page="...">, the sidebar markup
|
|
8
|
+
// (consoleSidebarHtml) inside a .le-app grid, and consoleShellScript
|
|
9
|
+
// before its own script. Pages talk to it through window.leShell:
|
|
10
|
+
// leShell.ready Promise of the /agents list
|
|
11
|
+
// leShell.setActive(name) highlight the agent this page shows
|
|
12
|
+
// leShell.renderHead(opts) agent header + tabs HTML (see below)
|
|
13
|
+
// leShell.setTab(tab) move the tab highlight without a reload
|
|
14
|
+
// leShell.esc(s) HTML-escape
|
|
15
|
+
// Same embedded-string conventions as the pages themselves: plain
|
|
16
|
+
// string concatenation and no backslashes inside the script, so nothing
|
|
17
|
+
// here needs double-escaping in the template literal.
|
|
18
|
+
const BRAND_MARK = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true">' +
|
|
19
|
+
'<path d="M3 7a4 4 0 1 0 4-4" stroke="var(--on-accent)" stroke-width="1.8" stroke-linecap="round"/>' +
|
|
20
|
+
'<path d="M7 1.2v3.6L9.2 3z" fill="var(--on-accent)"/></svg>';
|
|
21
|
+
export const consoleSidebarHtml = `<aside class="le-sidebar" aria-label="Console navigation">
|
|
22
|
+
<a class="le-brand" href="/agents"><span class="le-brand-mark">${BRAND_MARK}</span><span class="le-brand-name">LoopEngine</span></a>
|
|
23
|
+
<div class="le-nav-label"><span>Agents</span><a href="/agents?new=1">+ New</a></div>
|
|
24
|
+
<ul class="le-agent-list" id="leAgentList"></ul>
|
|
25
|
+
<nav class="le-foot" aria-label="Settings">
|
|
26
|
+
<a href="/config?section=models" data-le-nav="models">Models</a>
|
|
27
|
+
<a href="/config?section=gateways" data-le-nav="integrations">Integrations</a>
|
|
28
|
+
</nav>
|
|
29
|
+
</aside>`;
|
|
30
|
+
export const consoleShellScript = `<script>
|
|
31
|
+
(function () {
|
|
32
|
+
var page = document.body.getAttribute('data-le-page') || '';
|
|
33
|
+
var params = new URLSearchParams(location.search);
|
|
34
|
+
var app = document.querySelector('.le-app');
|
|
35
|
+
var list = document.getElementById('leAgentList');
|
|
36
|
+
var agents = [];
|
|
37
|
+
var activeAgent = null;
|
|
38
|
+
// Settings an installed ability declared scope "agent" for and this
|
|
39
|
+
// agent hasn't set — the only kind with no fallback, so the only kind
|
|
40
|
+
// that's certainly missing (see core/agent-env.ts). Filled in per agent
|
|
41
|
+
// after the list loads.
|
|
42
|
+
var toSet = {};
|
|
43
|
+
var TABS = [
|
|
44
|
+
['chat', 'Chat'], ['overview', 'Overview'], ['abilities', 'Abilities'], ['tools', 'Tools'],
|
|
45
|
+
['skills', 'Skills'], ['actauth', 'Permissions'], ['env', 'Environment']
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function esc(s) {
|
|
49
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
|
50
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function enc(s) { return encodeURIComponent(s); }
|
|
54
|
+
|
|
55
|
+
function tabHref(name, tab) {
|
|
56
|
+
if (tab === 'chat') return '/playground?agent=' + enc(name);
|
|
57
|
+
return '/agents/config?agent=' + enc(name) + (tab && tab !== 'overview' ? '&tab=' + enc(tab) : '');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Switching agents keeps you where you are: on a config tab you land on
|
|
61
|
+
// the same tab for the other agent, anywhere else you land in its Chat.
|
|
62
|
+
function agentHref(name) {
|
|
63
|
+
if (page === 'agent-config') return tabHref(name, params.get('tab') || 'overview');
|
|
64
|
+
return tabHref(name, 'chat');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function renderList() {
|
|
68
|
+
if (!list) return;
|
|
69
|
+
if (!agents.length) {
|
|
70
|
+
list.innerHTML = '<li class="le-agent-item"><span></span><span class="le-meta">No agents yet</span><span></span></li>';
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
list.innerHTML = agents.map(function (a) {
|
|
74
|
+
var n = toSet[a.name] || 0;
|
|
75
|
+
var meta = n ? '<span class="le-pill warn" title="Settings this agent must set before its tools work">' + n + ' to set</span>' : '';
|
|
76
|
+
return '<li><a class="le-agent-item" href="' + agentHref(a.name) + '"' + (a.name === activeAgent ? ' aria-current="page"' : '') + '>' +
|
|
77
|
+
'<span class="le-dot' + (n ? ' warn' : '') + '" aria-hidden="true"></span>' +
|
|
78
|
+
'<span class="le-name">' + esc(a.name) + '</span>' + meta + '</a></li>';
|
|
79
|
+
}).join('');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function envCountHtml(name) {
|
|
83
|
+
var n = toSet[name] || 0;
|
|
84
|
+
return n ? '<span class="le-count warn">' + n + ' to set</span>' : '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function loadToSet() {
|
|
88
|
+
agents.forEach(function (a) {
|
|
89
|
+
fetch('/agents/' + enc(a.name) + '/env')
|
|
90
|
+
.then(function (r) { return r.ok ? r.json() : []; })
|
|
91
|
+
.then(function (vars) {
|
|
92
|
+
toSet[a.name] = (vars || []).filter(function (v) { return v.slot === 'agent' && v.scope === 'agent' && !v.set; }).length;
|
|
93
|
+
renderList();
|
|
94
|
+
var slots = document.querySelectorAll('[data-le-env-count="' + a.name + '"]');
|
|
95
|
+
for (var i = 0; i < slots.length; i++) slots[i].innerHTML = envCountHtml(a.name);
|
|
96
|
+
})
|
|
97
|
+
.catch(function () {});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
var ready = fetch('/agents')
|
|
102
|
+
.then(function (r) { return r.json(); })
|
|
103
|
+
.then(function (data) {
|
|
104
|
+
agents = data.agents || [];
|
|
105
|
+
renderList();
|
|
106
|
+
loadToSet();
|
|
107
|
+
return agents;
|
|
108
|
+
})
|
|
109
|
+
.catch(function () { return []; });
|
|
110
|
+
|
|
111
|
+
// opts: { name, prompt, chips: [{ text, mono }], tab, counts: { tools, skills, abilities, actauth } }
|
|
112
|
+
function renderHead(opts) {
|
|
113
|
+
var counts = opts.counts || {};
|
|
114
|
+
var chips = (opts.chips || []).map(function (c) {
|
|
115
|
+
return '<span class="le-chip' + (c.mono ? ' mono' : '') + '">' + esc(c.text) + '</span>';
|
|
116
|
+
}).join('');
|
|
117
|
+
var tabs = TABS.map(function (t) {
|
|
118
|
+
var count = t[0] === 'env'
|
|
119
|
+
? '<span data-le-env-count="' + esc(opts.name) + '">' + envCountHtml(opts.name) + '</span>'
|
|
120
|
+
: (counts[t[0]] != null ? '<span class="le-count">' + counts[t[0]] + '</span>' : '');
|
|
121
|
+
return '<a class="le-tab" data-tab="' + t[0] + '" href="' + tabHref(opts.name, t[0]) + '" aria-selected="' + (t[0] === opts.tab) + '">' + t[1] + count + '</a>';
|
|
122
|
+
}).join('');
|
|
123
|
+
return '<div class="le-head"><div class="le-head-row">' +
|
|
124
|
+
'<div class="le-title"><button type="button" class="le-menu-btn" data-le-menu aria-label="Open navigation">☰</button>' +
|
|
125
|
+
'<div style="min-width:0"><h1>' + esc(opts.name) + '</h1>' + (opts.prompt ? '<p>' + esc(opts.prompt) + '</p>' : '') + '</div></div>' +
|
|
126
|
+
'<div class="le-chips">' + chips + '</div></div>' +
|
|
127
|
+
'<nav class="le-tabs" aria-label="Agent sections">' + tabs + '</nav></div>';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function setTab(tab) {
|
|
131
|
+
var links = document.querySelectorAll('.le-tab');
|
|
132
|
+
for (var i = 0; i < links.length; i++) links[i].setAttribute('aria-selected', String(links[i].getAttribute('data-tab') === tab));
|
|
133
|
+
if (tab === 'overview') params.delete('tab'); else params.set('tab', tab);
|
|
134
|
+
renderList();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Updates one tab's count badge in place (e.g. after an install).
|
|
138
|
+
function setCount(tab, n) {
|
|
139
|
+
var link = document.querySelector('.le-tab[data-tab="' + tab + '"]');
|
|
140
|
+
if (!link) return;
|
|
141
|
+
var badge = link.querySelector('.le-count');
|
|
142
|
+
if (!badge) { badge = document.createElement('span'); badge.className = 'le-count'; link.appendChild(badge); }
|
|
143
|
+
badge.textContent = String(n);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function setActive(name) {
|
|
147
|
+
activeAgent = name;
|
|
148
|
+
renderList();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
var foot = page === 'config' ? (params.get('section') === 'gateways' ? 'integrations' : 'models') : '';
|
|
152
|
+
var footLinks = document.querySelectorAll('[data-le-nav]');
|
|
153
|
+
for (var i = 0; i < footLinks.length; i++) {
|
|
154
|
+
if (footLinks[i].getAttribute('data-le-nav') === foot) footLinks[i].setAttribute('aria-current', 'page');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
document.addEventListener('click', function (ev) {
|
|
158
|
+
if (!app) return;
|
|
159
|
+
if (ev.target.closest('[data-le-menu]')) { app.classList.toggle('nav-open'); return; }
|
|
160
|
+
if (app.classList.contains('nav-open') && !ev.target.closest('.le-sidebar')) app.classList.remove('nav-open');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
window.leShell = { ready: ready, renderHead: renderHead, setTab: setTab, setActive: setActive, esc: esc, tabHref: tabHref, envCountHtml: envCountHtml, setCount: setCount };
|
|
164
|
+
})();
|
|
165
|
+
</script>`;
|
|
@@ -1,63 +1,169 @@
|
|
|
1
|
-
// Shared look for every browser page adapters/http.ts serves —
|
|
2
|
-
// (
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
1
|
+
// Shared look for every browser page adapters/http.ts serves — the agent
|
|
2
|
+
// console (agents-config-page.ts), its Chat tab (playground.ts), the
|
|
3
|
+
// agents home (agents-list-page.ts) and the Models/Integrations pages
|
|
4
|
+
// (global-config-page.ts). All four render inside the same app shell
|
|
5
|
+
// (web/console-shell.ts), so the shell's own classes (.le-*) live here
|
|
6
|
+
// too, alongside the shared base: color tokens, type, form controls,
|
|
7
|
+
// tables, pills. Each page still owns its page-specific rules (chat
|
|
8
|
+
// bubbles, trace events, the HTTP tool builder, ...) in its own <style>
|
|
9
|
+
// block, written against these same tokens.
|
|
10
|
+
//
|
|
11
|
+
// Colors match the loopengine.co site (teal accent). Fonts are a system
|
|
12
|
+
// stack that picks up Inter / JetBrains Mono when installed — never a
|
|
13
|
+
// web font download, so a self-hosted server works offline and loads
|
|
14
|
+
// nothing from a third party.
|
|
11
15
|
export const devUiCss = `
|
|
12
|
-
:root {
|
|
16
|
+
:root {
|
|
17
|
+
color-scheme: light;
|
|
18
|
+
--bg: #ffffff;
|
|
19
|
+
--surface: #f1f4f3;
|
|
20
|
+
--surface-2: #e7ece9;
|
|
21
|
+
--sidebar: #f6f8f7;
|
|
22
|
+
--ink: #10181a;
|
|
23
|
+
--ink-muted: #4c5c5c;
|
|
24
|
+
--ink-faint: #7c8d8b;
|
|
25
|
+
--line: #d7e0dc;
|
|
26
|
+
--code-bg: #e9efec;
|
|
27
|
+
--accent: #0c7a6a;
|
|
28
|
+
--accent-ink: #0c7a6a;
|
|
29
|
+
--accent-soft: #d7f0e9;
|
|
30
|
+
--on-accent: #ffffff;
|
|
31
|
+
--good: #1f7a3d; --good-soft: #dff3e5;
|
|
32
|
+
--warn: #9a5b00; --warn-soft: #fbeccf;
|
|
33
|
+
--bad: #b42318; --bad-soft: #fde4e1;
|
|
34
|
+
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
35
|
+
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
36
|
+
--radius: 8px;
|
|
37
|
+
}
|
|
38
|
+
@media (prefers-color-scheme: dark) {
|
|
39
|
+
:root {
|
|
40
|
+
color-scheme: dark;
|
|
41
|
+
--bg: #0b1112; --surface: #121a1b; --surface-2: #1a2425; --sidebar: #0e1516;
|
|
42
|
+
--ink: #e6ecea; --ink-muted: #9db0ac; --ink-faint: #647775; --line: #223030; --code-bg: #0f1718;
|
|
43
|
+
--accent: #57cdb8; --accent-ink: #7ee0cd; --accent-soft: #16302c; --on-accent: #06221d;
|
|
44
|
+
--good: #6fd08f; --good-soft: #12301d; --warn: #f0b75c; --warn-soft: #33270f; --bad: #f2877c; --bad-soft: #3a1714;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
13
47
|
* { box-sizing: border-box; }
|
|
48
|
+
html, body { height: 100%; }
|
|
14
49
|
body {
|
|
15
50
|
margin: 0;
|
|
16
|
-
font-family: -
|
|
17
|
-
|
|
18
|
-
|
|
51
|
+
font-family: var(--font-sans);
|
|
52
|
+
font-size: 14px;
|
|
53
|
+
line-height: 1.5;
|
|
54
|
+
background: var(--bg);
|
|
55
|
+
color: var(--ink);
|
|
19
56
|
}
|
|
20
|
-
a { color:
|
|
57
|
+
a { color: var(--accent-ink); }
|
|
58
|
+
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
21
59
|
select, input, textarea, button {
|
|
22
60
|
font: inherit;
|
|
61
|
+
font-size: 13px;
|
|
23
62
|
color: inherit;
|
|
24
|
-
background:
|
|
25
|
-
border: 1px solid
|
|
63
|
+
background: var(--bg);
|
|
64
|
+
border: 1px solid var(--line);
|
|
26
65
|
border-radius: 6px;
|
|
27
|
-
padding: 6px
|
|
66
|
+
padding: 6px 10px;
|
|
28
67
|
}
|
|
68
|
+
textarea { line-height: 1.45; }
|
|
29
69
|
button {
|
|
30
70
|
cursor: pointer;
|
|
31
|
-
|
|
71
|
+
font-weight: 500;
|
|
32
72
|
}
|
|
73
|
+
button:hover:not(:disabled) { background: var(--surface); }
|
|
74
|
+
button[type="submit"], button.primary {
|
|
75
|
+
background: var(--accent);
|
|
76
|
+
border-color: var(--accent);
|
|
77
|
+
color: var(--on-accent);
|
|
78
|
+
}
|
|
79
|
+
button[type="submit"]:hover:not(:disabled), button.primary:hover:not(:disabled) { background: var(--accent); filter: brightness(1.08); }
|
|
33
80
|
button:disabled { opacity: 0.5; cursor: default; }
|
|
34
81
|
code {
|
|
35
|
-
font-family:
|
|
36
|
-
background:
|
|
82
|
+
font-family: var(--font-mono);
|
|
83
|
+
background: var(--code-bg);
|
|
37
84
|
padding: 1px 5px;
|
|
38
85
|
border-radius: 4px;
|
|
39
|
-
font-size:
|
|
86
|
+
font-size: 12px;
|
|
40
87
|
}
|
|
41
88
|
.badge {
|
|
42
89
|
display: inline-block;
|
|
43
90
|
font-size: 11px;
|
|
44
|
-
font-
|
|
45
|
-
padding:
|
|
46
|
-
border-radius:
|
|
47
|
-
background:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.badge-
|
|
51
|
-
.
|
|
52
|
-
.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
91
|
+
font-weight: 500;
|
|
92
|
+
padding: 1px 8px;
|
|
93
|
+
border-radius: 999px;
|
|
94
|
+
background: var(--surface-2);
|
|
95
|
+
color: var(--ink-muted);
|
|
96
|
+
}
|
|
97
|
+
.badge-custom { background: var(--good-soft); color: var(--good); }
|
|
98
|
+
.badge-default { background: var(--surface-2); color: var(--ink-muted); }
|
|
99
|
+
.muted { color: var(--ink-muted); font-size: 13px; }
|
|
100
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
101
|
+
th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
102
|
+
th { color: var(--ink-faint); font-weight: 500; font-size: 12px; background: var(--surface); white-space: nowrap; }
|
|
103
|
+
tbody tr:last-child td { border-bottom: 0; }
|
|
104
|
+
|
|
105
|
+
/* ---------- pills: one shape for every allow/ask/deny or status word ---------- */
|
|
106
|
+
.le-pill { display: inline-block; font-size: 11.5px; font-weight: 500; padding: 1px 8px; border-radius: 999px; white-space: nowrap; }
|
|
107
|
+
.le-pill.allow, .le-pill.good { background: var(--good-soft); color: var(--good); }
|
|
108
|
+
.le-pill.ask, .le-pill.warn { background: var(--warn-soft); color: var(--warn); }
|
|
109
|
+
.le-pill.deny, .le-pill.bad { background: var(--bad-soft); color: var(--bad); }
|
|
110
|
+
.le-pill.neutral { background: var(--surface-2); color: var(--ink-muted); }
|
|
111
|
+
.le-pill.accent { background: var(--accent-soft); color: var(--accent-ink); }
|
|
112
|
+
|
|
113
|
+
/* ---------- app shell (web/console-shell.ts) ---------- */
|
|
114
|
+
.le-app { display: grid; grid-template-columns: 240px minmax(0, 1fr); height: 100%; }
|
|
115
|
+
.le-sidebar { background: var(--sidebar); border-right: 1px solid var(--line); display: flex; flex-direction: column; min-height: 0; }
|
|
116
|
+
.le-brand { display: flex; align-items: center; gap: 8px; padding: 16px 16px 12px; text-decoration: none; color: var(--ink); }
|
|
117
|
+
.le-brand-mark { width: 22px; height: 22px; border-radius: 6px; background: var(--accent); display: grid; place-items: center; flex-shrink: 0; }
|
|
118
|
+
.le-brand-name { font-weight: 600; font-size: 16px; letter-spacing: -0.01em; }
|
|
119
|
+
.le-nav-label { padding: 10px 16px 6px; font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase; color: var(--ink-faint); display: flex; justify-content: space-between; align-items: center; }
|
|
120
|
+
.le-nav-label a { text-transform: none; letter-spacing: 0; font-weight: 500; font-size: 12px; text-decoration: none; }
|
|
121
|
+
.le-agent-list { list-style: none; margin: 0; padding: 0 8px; overflow-y: auto; flex: 1; min-height: 0; }
|
|
122
|
+
.le-agent-item { display: grid; grid-template-columns: 8px minmax(0, 1fr) auto; align-items: center; gap: 10px; padding: 7px 8px; border-radius: 6px; color: var(--ink); text-decoration: none; }
|
|
123
|
+
.le-agent-item:hover { background: var(--surface-2); }
|
|
124
|
+
.le-agent-item[aria-current="page"] { background: var(--accent-soft); }
|
|
125
|
+
.le-agent-item .le-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
126
|
+
.le-agent-item .le-meta { font-size: 12px; color: var(--ink-faint); }
|
|
127
|
+
.le-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--good); }
|
|
128
|
+
.le-dot.warn { background: var(--warn); }
|
|
129
|
+
.le-foot { border-top: 1px solid var(--line); padding: 8px; display: grid; gap: 2px; }
|
|
130
|
+
.le-foot a { padding: 7px 8px; border-radius: 6px; color: var(--ink-muted); text-decoration: none; }
|
|
131
|
+
.le-foot a:hover { background: var(--surface-2); color: var(--ink); }
|
|
132
|
+
.le-foot a[aria-current="page"] { background: var(--accent-soft); color: var(--ink); }
|
|
133
|
+
.le-main { display: flex; flex-direction: column; min-width: 0; min-height: 0; overflow: hidden; }
|
|
134
|
+
.le-scroll { flex: 1; min-height: 0; overflow-y: auto; }
|
|
135
|
+
.le-page { padding: 24px; max-width: 1100px; display: grid; gap: 24px; }
|
|
136
|
+
.le-head { padding: 20px 24px 0; display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; border-bottom: 1px solid var(--line); }
|
|
137
|
+
.le-head.plain { padding-bottom: 16px; }
|
|
138
|
+
.le-head-row { display: flex; justify-content: space-between; align-items: flex-start; gap: 16px; flex-wrap: wrap; }
|
|
139
|
+
.le-title { display: flex; gap: 10px; align-items: flex-start; min-width: 0; }
|
|
140
|
+
.le-title h1 { font-size: 22px; font-weight: 600; letter-spacing: -0.01em; margin: 0; overflow-wrap: anywhere; }
|
|
141
|
+
.le-title p { margin: 4px 0 0; color: var(--ink-muted); max-width: 72ch; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
|
142
|
+
.le-chips { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
|
143
|
+
.le-chip { font-size: 12px; padding: 2px 8px; border-radius: 999px; background: var(--surface); border: 1px solid var(--line); color: var(--ink-muted); white-space: nowrap; }
|
|
144
|
+
.le-chip.mono { font-family: var(--font-mono); font-size: 11.5px; }
|
|
145
|
+
.le-tabs { display: flex; gap: 2px; overflow-x: auto; scrollbar-width: none; }
|
|
146
|
+
.le-tab { padding: 10px 12px; color: var(--ink-muted); border-bottom: 2px solid transparent; white-space: nowrap; display: flex; gap: 6px; align-items: center; text-decoration: none; }
|
|
147
|
+
.le-tab:hover { color: var(--ink); }
|
|
148
|
+
.le-tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--accent); font-weight: 500; }
|
|
149
|
+
.le-count { font-size: 11px; background: var(--surface-2); color: var(--ink-muted); padding: 0 6px; border-radius: 999px; }
|
|
150
|
+
.le-count.warn { background: var(--warn-soft); color: var(--warn); }
|
|
151
|
+
.le-menu-btn { display: none; border: 0; background: transparent; padding: 2px 6px; font-size: 18px; line-height: 1; color: var(--ink-muted); }
|
|
152
|
+
.le-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
|
|
153
|
+
.le-card { border: 1px solid var(--line); border-radius: var(--radius); padding: 14px; display: grid; gap: 6px; align-content: start; color: var(--ink); text-decoration: none; background: var(--bg); }
|
|
154
|
+
a.le-card:hover { border-color: var(--accent); }
|
|
155
|
+
.le-card .le-card-label { font-size: 12px; color: var(--ink-faint); display: flex; justify-content: space-between; }
|
|
156
|
+
.le-card .le-card-value { font-size: 20px; font-weight: 600; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
|
|
157
|
+
.le-card .le-card-detail { font-size: 12.5px; color: var(--ink-muted); overflow-wrap: anywhere; }
|
|
158
|
+
.le-card.attention { border-color: var(--warn); box-shadow: inset 0 3px 0 var(--warn-soft); }
|
|
159
|
+
.le-table-wrap { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--radius); }
|
|
160
|
+
.le-empty { color: var(--ink-faint); padding: 24px; text-align: center; border: 1px dashed var(--line); border-radius: var(--radius); }
|
|
161
|
+
@media (max-width: 860px) {
|
|
162
|
+
.le-app { grid-template-columns: minmax(0, 1fr); }
|
|
163
|
+
.le-sidebar { position: fixed; inset: 0 auto 0 0; width: 264px; z-index: 20; transform: translateX(-100%); transition: transform .2s; box-shadow: 0 0 24px rgba(0, 0, 0, .25); }
|
|
164
|
+
.le-app.nav-open .le-sidebar { transform: none; }
|
|
165
|
+
.le-menu-btn { display: block; }
|
|
166
|
+
.le-head, .le-page { padding-left: 16px; padding-right: 16px; }
|
|
59
167
|
}
|
|
60
|
-
|
|
61
|
-
.topnav a:hover { text-decoration: underline; }
|
|
62
|
-
.topnav a.active { color: light-dark(#1a1a1e, #e8e8ea); font-weight: 600; }
|
|
168
|
+
@media (prefers-reduced-motion: reduce) { .le-sidebar { transition: none; } }
|
|
63
169
|
`;
|
package/dist/web/env-admin.js
CHANGED
|
@@ -104,12 +104,10 @@ export function listDeclaredEnvVars(agentName) {
|
|
|
104
104
|
const scope = envScopeOf(decl);
|
|
105
105
|
if (scope !== 'agent')
|
|
106
106
|
upsert('shared', abilityName, decl, decl.description);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
upsert('agent', abilityName, decl, `Set per agent — never shared with or borrowed from other agents.${decl.description ? ` ${decl.description}` : ''}`);
|
|
112
|
-
}
|
|
107
|
+
// `slot` and `scope` already say whose value a row is and whether
|
|
108
|
+
// it falls back, so the agent row keeps the ability's own words.
|
|
109
|
+
if (scope !== 'shared')
|
|
110
|
+
upsert('agent', abilityName, decl, decl.description);
|
|
113
111
|
}
|
|
114
112
|
}
|
|
115
113
|
// A name that's `agent`-scoped by any declaring ability is never read
|