loopengine 0.1.62 → 0.1.64

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.
@@ -26,108 +26,37 @@
26
26
  // concatenation, not a nested template literal, for the same
27
27
  // backtick/${}-escaping reason playground.ts's own header comment explains.
28
28
  import { devUiCss } from './dev-ui-styles.js';
29
+ import { consoleShellScript, consoleSidebarHtml } from './console-shell.js';
29
30
  export const agentsConfigPageHtml = `<!doctype html>
30
31
  <html lang="en">
31
32
  <head>
32
33
  <meta charset="utf-8">
33
34
  <meta name="viewport" content="width=device-width, initial-scale=1">
34
- <title>LoopEngine Agents</title>
35
+ <title>LoopEngine Console</title>
35
36
  <style>${devUiCss}
36
- body {
37
- display: flex;
38
- flex-direction: column;
39
- height: 100vh;
40
- }
41
- /* Outer Agents/Models/Gateways section sidebar — matches
42
- web/global-config-page.ts's own left nav, so this page (with
43
- Agents pre-selected) and /config (with Models pre-selected) read as
44
- two views of the same shell rather than unrelated pages. Models and
45
- Gateways are real links out to /config?section=..., not client-side
46
- panels rendered here too — that data/logic already lives in
47
- global-config-page.ts, duplicating it here would just be two places
48
- it could drift apart. Named .section-sidebar (not .sidebar) to avoid
49
- colliding with nav.sidebar right below, which is the agent picker,
50
- not this new outer one. */
51
- .page-body { flex: 1; display: flex; min-height: 0; }
52
- nav.section-sidebar {
53
- width: 150px;
54
- flex-shrink: 0;
55
- border-right: 1px solid light-dark(#ddd, #333);
56
- padding: 6px;
57
- }
58
- nav.section-sidebar a {
59
- display: block;
60
- border-radius: 6px;
61
- padding: 8px 10px;
62
- font-size: 13px;
63
- text-decoration: none;
64
- color: inherit;
65
- }
66
- nav.section-sidebar a:hover { background: light-dark(#eee, #26262b); }
67
- nav.section-sidebar a.active { background: light-dark(#dbeafe, #1e3a5f); font-weight: 600; }
68
- .layout { flex: 1; display: flex; min-height: 0; }
69
- nav.sidebar {
70
- width: 220px;
71
- flex-shrink: 0;
72
- border-right: 1px solid light-dark(#ddd, #333);
73
- overflow-y: auto;
74
- display: flex;
75
- flex-direction: column;
76
- }
77
- #agentList { list-style: none; margin: 0; padding: 6px; flex: 1; }
78
- #agentList li { border-radius: 6px; }
79
- #agentList li a {
80
- display: block;
81
- padding: 8px 10px;
82
- border-radius: 6px;
83
- font-size: 13px;
84
- font-family: ui-monospace, monospace;
85
- color: inherit;
86
- text-decoration: none;
87
- }
88
- #agentList li a:hover { background: light-dark(#eee, #26262b); }
89
- #agentList li.active a { background: light-dark(#dbeafe, #1e3a5f); font-weight: 600; }
90
- main {
91
- flex: 1;
92
- overflow-y: auto;
93
- padding: 20px 28px;
94
- }
95
- #empty { color: light-dark(#666, #999); font-size: 13px; }
96
- h2 { font-size: 20px; margin: 0 0 4px; font-family: ui-monospace, monospace; }
97
- .tabs {
98
- display: flex;
99
- gap: 18px;
100
- margin: 14px 0 20px;
101
- border-bottom: 1px solid light-dark(#ddd, #333);
102
- }
103
- .tabs button {
104
- background: none;
105
- border: none;
106
- border-bottom: 2px solid transparent;
107
- padding: 8px 2px;
108
- font-size: 13px;
109
- cursor: pointer;
110
- color: light-dark(#666, #999);
111
- }
112
- .tabs button:hover { color: light-dark(#1a1a1e, #e8e8ea); }
113
- .tabs button.active {
114
- color: light-dark(#1a1a1e, #e8e8ea);
115
- border-bottom-color: light-dark(#1d4ed8, #60a5fa);
116
- font-weight: 600;
117
- }
118
- section { margin-bottom: 22px; }
37
+ /* Each tab panel is a .le-page column inside the shell's scrolling
38
+ main area (see web/console-shell.ts); everything below styles the
39
+ panels' own content. */
40
+ #empty { color: var(--ink-muted); font-size: 13px; padding: 24px; }
41
+ .env-cell { display: grid; gap: 6px; }
42
+ .env-desc { max-width: 340px; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
43
+ form.add-source.env-inline { border: 0; padding: 0; display: flex; flex-wrap: wrap; gap: 6px; max-width: 320px; }
44
+ form.add-source.env-inline input, form.add-source.env-inline select { flex: 1 1 140px; width: auto; min-width: 0; }
45
+ form.add-source.env-inline textarea { flex: 1 1 100%; }
46
+ form.add-source.env-inline button { padding: 4px 10px; }
47
+ .tab-panel .le-table-wrap table { border: 0; border-radius: 0; }
48
+ section { margin-bottom: 28px; }
119
49
  section h3 {
120
- font-size: 12px;
121
- text-transform: uppercase;
122
- letter-spacing: 0.04em;
123
- color: light-dark(#666, #999);
124
- margin: 0 0 8px;
125
- border-bottom: 1px solid light-dark(#eee, #2a2a2e);
126
- padding-bottom: 4px;
50
+ font-size: 15px;
51
+ font-weight: 600;
52
+ color: var(--ink);
53
+ margin: 0 0 10px;
127
54
  display: flex;
128
55
  align-items: center;
129
56
  justify-content: space-between;
57
+ gap: 12px;
130
58
  }
59
+ section > table, .tab-panel table { border: 1px solid var(--line); border-radius: var(--radius); border-collapse: separate; border-spacing: 0; overflow: hidden; }
131
60
  .hint-btn {
132
61
  font-size: 11px;
133
62
  text-transform: none;
@@ -137,18 +66,18 @@ export const agentsConfigPageHtml = `<!doctype html>
137
66
  .admin-subsection {
138
67
  margin-top: 14px;
139
68
  padding-top: 14px;
140
- border-top: 1px dashed light-dark(#eee, #2a2a2e);
69
+ border-top: 1px dashed var(--line);
141
70
  }
142
71
  .admin-subsection h4 {
143
72
  font-size: 12px;
144
73
  text-transform: uppercase;
145
74
  letter-spacing: 0.04em;
146
- color: light-dark(#666, #999);
75
+ color: var(--ink-muted);
147
76
  margin: 0 0 8px;
148
77
  }
149
78
  pre {
150
- background: light-dark(#fff, #26262b);
151
- border: 1px solid light-dark(#ddd, #3a3a3e);
79
+ background: var(--bg);
80
+ border: 1px solid var(--line);
152
81
  border-radius: 6px;
153
82
  padding: 10px 12px;
154
83
  font-size: 12px;
@@ -160,29 +89,29 @@ export const agentsConfigPageHtml = `<!doctype html>
160
89
  th, td {
161
90
  text-align: left;
162
91
  padding: 6px 8px;
163
- border-bottom: 1px solid light-dark(#eee, #2a2a2e);
92
+ border-bottom: 1px solid var(--line);
164
93
  vertical-align: top;
165
94
  }
166
- th { color: light-dark(#666, #999); font-weight: 600; }
167
- .decision-allow { color: light-dark(#166534, #86efac); }
168
- .decision-ask { color: light-dark(#92400e, #fcd34d); }
169
- .decision-deny { color: light-dark(#991b1b, #f87171); }
95
+ th { color: var(--ink-muted); font-weight: 600; }
96
+ .decision-allow { color: var(--good); }
97
+ .decision-ask { color: var(--warn); }
98
+ .decision-deny { color: var(--bad); }
170
99
  .kv { display: grid; grid-template-columns: max-content 1fr; gap: 4px 16px; font-size: 13px; }
171
- .kv dt { color: light-dark(#666, #999); }
100
+ .kv dt { color: var(--ink-muted); }
172
101
  .kv dd { margin: 0; font-family: ui-monospace, monospace; }
173
- .error { color: light-dark(#991b1b, #f87171); font-size: 13px; }
174
- .hint { font-size: 11px; color: light-dark(#666, #999); }
102
+ .error { color: var(--bad); font-size: 13px; }
103
+ .hint { font-size: 11px; color: var(--ink-muted); }
175
104
  #playgroundLink { font-size: 12px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-weight: normal; text-decoration: none; margin-left: 8px; }
176
105
  .source {
177
- border: 1px solid light-dark(#ddd, #3a3a3e);
106
+ border: 1px solid var(--line);
178
107
  border-radius: 8px;
179
108
  padding: 12px 14px;
180
109
  margin-bottom: 10px;
181
110
  }
182
111
  .source-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
183
112
  .source-head h4 { margin: 0; font-size: 14px; font-family: ui-monospace, monospace; }
184
- .status-ok { color: light-dark(#166534, #86efac); }
185
- .status-error { color: light-dark(#991b1b, #f87171); }
113
+ .status-ok { color: var(--good); }
114
+ .status-error { color: var(--bad); }
186
115
  .tool-list { margin: 8px 0 0; padding-left: 0; font-size: 12px; }
187
116
  .tool-list li {
188
117
  margin-bottom: 2px;
@@ -193,11 +122,11 @@ export const agentsConfigPageHtml = `<!doctype html>
193
122
  .delete-btn {
194
123
  flex-shrink: 0;
195
124
  font-size: 12px;
196
- color: light-dark(#991b1b, #f87171);
197
- background: light-dark(#fee2e2, #3a1f1f);
198
- border-color: light-dark(#f3b4b4, #6b3232);
125
+ color: var(--bad);
126
+ background: var(--bad-soft);
127
+ border-color: var(--bad);
199
128
  }
200
- .delete-btn:hover { background: light-dark(#fecaca, #4a1f1f); }
129
+ .delete-btn:hover { background: var(--bad-soft); }
201
130
  /* Explicit rather than relying on inherited font-size: .edit-rule-btn
202
131
  sits inside a 12px table, but .edit-skill-btn sits in .source-head
203
132
  (no font-size of its own, so it'd otherwise inherit the page's
@@ -218,7 +147,7 @@ export const agentsConfigPageHtml = `<!doctype html>
218
147
  once there's nowhere left to wrap normally. */
219
148
  .tool-picker-label { flex: 1; min-width: 0; overflow-wrap: break-word; }
220
149
  form.add-source {
221
- border: 1px solid light-dark(#ddd, #3a3a3e);
150
+ border: 1px solid var(--line);
222
151
  border-radius: 8px;
223
152
  padding: 14px;
224
153
  display: grid;
@@ -247,9 +176,9 @@ export const agentsConfigPageHtml = `<!doctype html>
247
176
  .body-field-head { display: flex; align-items: center; justify-content: space-between; }
248
177
  .md-toggle { display: flex; gap: 4px; }
249
178
  .md-toggle-btn { padding: 2px 8px; font-size: 11px; background: transparent; }
250
- .md-toggle-btn.active { background: light-dark(#e8e8ea, #333338); font-weight: 600; }
179
+ .md-toggle-btn.active { background: var(--surface-2); font-weight: 600; }
251
180
  .markdown-body {
252
- border: 1px solid light-dark(#ccc, #444);
181
+ border: 1px solid var(--line);
253
182
  border-radius: 6px;
254
183
  padding: 10px 14px;
255
184
  min-height: 280px;
@@ -257,17 +186,17 @@ export const agentsConfigPageHtml = `<!doctype html>
257
186
  overflow-y: auto;
258
187
  }
259
188
  .markdown-body h1, .markdown-body h2 {
260
- border-bottom: 1px solid light-dark(#eee, #333338);
189
+ border-bottom: 1px solid var(--line);
261
190
  padding-bottom: 4px;
262
191
  }
263
192
  .markdown-body pre {
264
- background: light-dark(#f6f8fa, #26262b);
193
+ background: var(--surface);
265
194
  padding: 10px 12px;
266
195
  border-radius: 6px;
267
196
  overflow-x: auto;
268
197
  }
269
198
  .markdown-body code {
270
- background: light-dark(#eee, #2a2a2e);
199
+ background: var(--line);
271
200
  padding: 1px 5px;
272
201
  border-radius: 4px;
273
202
  font-family: ui-monospace, monospace;
@@ -284,7 +213,7 @@ export const agentsConfigPageHtml = `<!doctype html>
284
213
  .tool-picker-list {
285
214
  max-height: 240px;
286
215
  overflow-y: auto;
287
- border: 1px solid light-dark(#ddd, #3a3a3e);
216
+ border: 1px solid var(--line);
288
217
  border-radius: 6px;
289
218
  padding: 6px 8px;
290
219
  }
@@ -304,7 +233,7 @@ export const agentsConfigPageHtml = `<!doctype html>
304
233
  gap: 8px;
305
234
  padding: 5px 2px;
306
235
  font-size: 12px;
307
- border-bottom: 1px solid light-dark(#eee, #2a2a2e);
236
+ border-bottom: 1px solid var(--line);
308
237
  }
309
238
  .tool-picker-item:last-child { border-bottom: none; }
310
239
  /* Higher specificity than "form.add-source input { width: 100%; }" —
@@ -372,31 +301,17 @@ export const agentsConfigPageHtml = `<!doctype html>
372
301
  }
373
302
  </style>
374
303
  </head>
375
- <body>
376
- <nav class="topnav">
377
- <a href="/agents">Agents</a>
378
- <a href="/agents/config" class="active">Config</a>
379
- <a href="/playground">Playground</a>
380
- </nav>
381
- <div class="page-body">
382
- <nav class="section-sidebar">
383
- <a href="/agents/config" class="active">Agents</a>
384
- <a href="/config?section=models">Models</a>
385
- <a href="/config?section=gateways">Gateways</a>
386
- </nav>
387
- <div class="layout">
388
- <nav class="sidebar">
389
- <ul id="agentList"></ul>
390
- </nav>
391
- <main>
392
- <div id="empty">Pick an agent to see its config.</div>
393
- <div id="detail" style="display:none"></div>
304
+ <body data-le-page="agent-config">
305
+ <div class="le-app">
306
+ ${consoleSidebarHtml}
307
+ <main class="le-main">
308
+ <div id="empty">Loading agents&hellip;</div>
309
+ <div id="detail" style="display:none;flex-direction:column;flex:1;min-height:0"></div>
394
310
  </main>
395
311
  </div>
396
- </div>
312
+ ${consoleShellScript}
397
313
  <script>
398
314
  (function () {
399
- var agentList = document.getElementById('agentList');
400
315
  var empty = document.getElementById('empty');
401
316
  var detail = document.getElementById('detail');
402
317
  var currentName = null;
@@ -574,14 +489,31 @@ export const agentsConfigPageHtml = `<!doctype html>
574
489
 
575
490
  // ---- Overview tab ----
576
491
 
492
+ // Whether a call to this tool runs on its own, waits for a person, or
493
+ // never runs — read off the agent's resolved rules (cfg.permissions).
494
+ // A tool whose rules disagree by tenant/environment, or depend on a
495
+ // "when" condition, shows "depends" rather than picking one.
496
+ function runsPill(toolName) {
497
+ var perms = currentCfg && currentCfg.permissions;
498
+ if (!perms) return '';
499
+ var matching = perms.rules.filter(function (r) { return r.tool === toolName || r.tool === '*'; });
500
+ var decisions = [];
501
+ matching.forEach(function (r) { if (decisions.indexOf(r.decision) === -1) decisions.push(r.decision); });
502
+ var conditional = matching.some(function (r) { return r.when; });
503
+ if (!matching.length) return '<span class="le-pill ' + perms.defaultDecision + '" title="No rule names this tool; the default applies">' + escapeHtml(perms.defaultDecision) + '</span>';
504
+ if (decisions.length === 1 && !conditional) return '<span class="le-pill ' + decisions[0] + '">' + escapeHtml(decisions[0]) + '</span>';
505
+ return '<span class="le-pill neutral" title="' + escapeHtml(decisions.join(' / ')) + ', depending on tenant, environment or condition">depends</span>';
506
+ }
507
+
577
508
  function renderTools(tools) {
578
- if (!tools.length) return '<p class="muted">No tools.</p>';
509
+ if (!tools.length) return '<p class="muted">None.</p>';
579
510
  var rows = tools.map(function (t) {
580
511
  return '<tr><td><code>' + escapeHtml(t.name) + '</code></td>' +
581
512
  '<td>' + escapeHtml(t.description) + '</td>' +
513
+ '<td>' + runsPill(t.name) + '</td>' +
582
514
  '<td>' + (t.safe ? 'yes' : 'no') + '</td></tr>';
583
515
  }).join('');
584
- return '<table><thead><tr><th>Name</th><th>Description</th><th>Parallel-safe</th></tr></thead><tbody>' + rows + '</tbody></table>';
516
+ return '<table><thead><tr><th>Name</th><th>What it does</th><th>Runs</th><th>Parallel-safe</th></tr></thead><tbody>' + rows + '</tbody></table>';
585
517
  }
586
518
 
587
519
  // Local tools' own table, unlike renderTools above (shared by Agent as
@@ -593,17 +525,18 @@ export const agentsConfigPageHtml = `<!doctype html>
593
525
  // has no spec to repopulate an edit form from, so it just gets a blank
594
526
  // cell — not a button that would 404 the moment it's clicked.
595
527
  function renderLocalTools(tools) {
596
- if (!tools.length) return '<p class="muted">No tools.</p>';
528
+ if (!tools.length) return '<p class="muted">No tools yet. Add one below, install an ability, or connect an integration.</p>';
597
529
  var rows = tools.map(function (t) {
598
530
  var actions = t.httpToolEditable
599
531
  ? '<button type="button" class="edit-http-tool-btn" data-name="' + escapeHtml(t.name) + '">Edit</button>'
600
532
  : '';
601
533
  return '<tr><td><code>' + escapeHtml(t.name) + '</code></td>' +
602
534
  '<td>' + escapeHtml(t.description) + '</td>' +
535
+ '<td>' + runsPill(t.name) + '</td>' +
603
536
  '<td>' + (t.safe ? 'yes' : 'no') + '</td>' +
604
537
  '<td>' + actions + '</td></tr>';
605
538
  }).join('');
606
- return '<table><thead><tr><th>Name</th><th>Description</th><th>Parallel-safe</th><th></th></tr></thead><tbody>' + rows + '</tbody></table>';
539
+ return '<table><thead><tr><th>Name</th><th>What it does</th><th>Runs</th><th>Parallel-safe</th><th></th></tr></thead><tbody>' + rows + '</tbody></table>';
607
540
  }
608
541
 
609
542
  // Overview's own tools table, unlike the Tools tab's three separately-
@@ -648,6 +581,34 @@ export const agentsConfigPageHtml = `<!doctype html>
648
581
  return '<table><thead><tr><th>Name</th><th>Description</th></tr></thead><tbody>' + rows + '</tbody></table>';
649
582
  }
650
583
 
584
+ // The top of Overview: one card per other tab, each a summary that
585
+ // links to the tab with the detail — Overview itself no longer repeats
586
+ // the full tool/skill/rule tables those tabs already show.
587
+ function renderOverviewCards(cfg) {
588
+ var rules = cfg.permissions.rules;
589
+ var asks = rules.filter(function (r) { return r.decision === 'ask'; }).length;
590
+ var toolNames = cfg.tools.map(function (t) { return t.name; });
591
+ var modelValue = typeof cfg.model === 'string' ? cfg.model : (cfg.model.model || cfg.model.provider);
592
+ var modelDetail = typeof cfg.model === 'string' ? 'custom createModelCall' : cfg.model.provider + (cfg.model.maxTokens != null ? ' \u00b7 max tokens ' + cfg.model.maxTokens : '');
593
+ function card(tab, label, value, detailText) {
594
+ var inner = '<span class="le-card-label"><span>' + label + '</span>' + (tab ? '<span aria-hidden="true">&rarr;</span>' : '') + '</span>' +
595
+ '<span class="le-card-value">' + value + '</span>' +
596
+ '<span class="le-card-detail">' + detailText + '</span>';
597
+ return tab
598
+ ? '<a class="le-card" data-tab="' + tab + '" href="' + leShell.tabHref(cfg.name, tab) + '">' + inner + '</a>'
599
+ : '<div class="le-card">' + inner + '</div>';
600
+ }
601
+ return '<div class="le-cards" style="margin-bottom:28px">' +
602
+ card(null, 'Model', escapeHtml(modelValue), escapeHtml(modelDetail)) +
603
+ card('tools', 'Tools', String(cfg.tools.length), escapeHtml(toolNames.slice(0, 3).join(', ') + (toolNames.length > 3 ? ', \u2026' : '')) || 'None yet') +
604
+ card('actauth', 'Permissions', rules.length + (rules.length === 1 ? ' rule' : ' rules'),
605
+ (asks ? asks + ' need approval \u00b7 ' : '') + 'unmatched calls: ' + escapeHtml(cfg.permissions.defaultDecision)) +
606
+ card('skills', 'Skills', String(cfg.skills.length), escapeHtml(cfg.skills.map(function (k) { return k.name; }).join(', ')) || 'None yet') +
607
+ card('abilities', 'Abilities', 'Browse', 'Add tools, skills and rules in one install') +
608
+ card('env', 'Environment', 'Settings', '<span data-le-env-count="' + escapeHtml(cfg.name) + '">' + leShell.envCountHtml(cfg.name) + '</span> Values your installed abilities read') +
609
+ '</div>';
610
+ }
611
+
651
612
  function renderOverviewHtml(cfg) {
652
613
  var systemPromptSection =
653
614
  '<h3>System prompt <button type="button" class="edit-btn" id="editSystemPromptBtn">Edit</button></h3>' +
@@ -703,11 +664,9 @@ export const agentsConfigPageHtml = `<!doctype html>
703
664
  '</form>';
704
665
  }
705
666
 
706
- return '<section>' + systemPromptSection + '</section>' +
667
+ return renderOverviewCards(cfg) +
668
+ '<section>' + systemPromptSection + '</section>' +
707
669
  '<section>' + modelSection + '</section>' +
708
- '<section><h3>Skills (' + cfg.skills.length + ')</h3>' + renderSkills(cfg.skills) + '</section>' +
709
- '<section><h3>Tools (' + cfg.tools.length + ')</h3>' + renderToolsWithType(cfg) + '</section>' +
710
- '<section><h3>ActAuth</h3>' + renderRules(cfg.permissions) + '</section>' +
711
670
  '<section><h3>Hooks</h3><dl class="kv">' +
712
671
  '<dt>sessionIdFor</dt><dd>' + badge(cfg.sessionIdFor.indexOf('custom') === 0 ? 'custom' : 'default') + ' <span class="muted">' + escapeHtml(cfg.sessionIdFor) + '</span></dd>' +
713
672
  '<dt>tenantFor</dt><dd>' + badge(cfg.tenantFor.indexOf('custom') === 0 ? 'custom' : 'default') + ' <span class="muted">' + escapeHtml(cfg.tenantFor) + '</span></dd>' +
@@ -943,20 +902,22 @@ export const agentsConfigPageHtml = `<!doctype html>
943
902
  '<td><code>' + escapeHtml(r.name || '') + '</code></td>' +
944
903
  '<td><code>' + escapeHtml(r.scope) + '</code></td>' +
945
904
  '<td><code>' + escapeHtml(r.tool) + '</code></td>' +
946
- '<td class="' + decisionClass(r.decision) + '">' + escapeHtml(r.decision) + '</td>' +
905
+ '<td><span class="le-pill ' + escapeHtml(r.decision) + '">' + escapeHtml(r.decision) + '</span></td>' +
947
906
  '<td>' + actionsHtml + '</td>' +
948
907
  '</tr>';
949
908
  }
950
909
 
951
910
  function renderActauthConfigHtml(cfg) {
952
911
  var rulesHtml = cfg.rules.length
953
- ? '<table><thead><tr><th>Name</th><th>Scope</th><th>Tool</th><th>Decision</th><th></th></tr></thead><tbody>' +
912
+ ? '<table><thead><tr><th>Name</th><th>Who (tenant / environment)</th><th>Tool</th><th>Decision</th><th></th></tr></thead><tbody>' +
954
913
  cfg.rules.map(renderActauthRuleRow).join('') + '</tbody></table>'
955
914
  : '<p class="hint">No explicit rules — every tool call falls through to the default decision.</p>';
956
915
 
957
- return '<section><h3>Default decision</h3>' +
958
- '<form class="add-source" id="defaultDecisionForm" style="max-width:220px">' +
959
- '<label>Applies when no rule matches' +
916
+ return '<p class="muted" style="margin:0 0 20px">Decide which tool calls run on their own, which wait for a person, and which never run. Stored in <code>actauth.yml</code> in this agent&#39;s folder.</p>' +
917
+ '<section><h3>Rules (' + cfg.rules.length + ')</h3>' + rulesHtml + '</section>' +
918
+ '<section><h3>When no rule matches</h3>' +
919
+ '<form class="add-source" id="defaultDecisionForm" style="max-width:260px">' +
920
+ '<label>Decision' +
960
921
  '<select name="defaultDecision" id="defaultDecisionSelect">' +
961
922
  '<option value="allow"' + (cfg.defaultDecision === 'allow' ? ' selected' : '') + '>allow</option>' +
962
923
  '<option value="ask"' + (cfg.defaultDecision === 'ask' ? ' selected' : '') + '>ask</option>' +
@@ -966,13 +927,12 @@ export const agentsConfigPageHtml = `<!doctype html>
966
927
  '<button type="submit">Save</button>' +
967
928
  '<div id="defaultDecisionError" class="error"></div>' +
968
929
  '</form></section>' +
969
- '<section><h3>Rules (' + cfg.rules.length + ')</h3>' + rulesHtml + '</section>' +
970
930
  '<section><h3 id="ruleFormHeading">Add a rule</h3>' +
971
931
  '<form class="add-source" id="ruleForm">' +
972
932
  '<label>Name' +
973
933
  '<input name="name" id="ruleNameInput" required placeholder="my-rule-name">' +
974
934
  '</label>' +
975
- '<label>Scope <span class="hint">(tenant/environment — e.g. "default/production" or "*/*"; the agent segment is appended automatically)</span>' +
935
+ '<label>Who <span class="hint">(tenant/environment, e.g. "default/production" or "*/*" for everyone)</span>' +
976
936
  '<input name="scope" id="ruleScopeInput" required placeholder="default/production">' +
977
937
  '</label>' +
978
938
  '<label>Tool' +
@@ -1157,104 +1117,86 @@ export const agentsConfigPageHtml = `<!doctype html>
1157
1117
  return '<section id="envSection"><div id="envContent"><p class="hint">Loading&hellip;</p></div></section>';
1158
1118
  }
1159
1119
 
1160
- function renderEnvRow(v) {
1161
- // More than one ability declaring the same name doesn't get resolved
1162
- // here — there's one .env per project, so both read whatever single
1163
- // value ends up set, whether that's actually correct or a coincidence
1164
- // (see env-admin.ts's own doc comment). Naming every declaring
1165
- // ability instead of just the first one seen makes that visible to
1166
- // whoever's looking, instead of silently hiding all but one — shown
1167
- // here (not a separate "Required by" column, now that rows are
1168
- // already grouped under one ability's own card) only when there
1169
- // actually is more than one, since inside its own card which ability
1170
- // this is is already obvious from context.
1171
- var sharedNote = v.abilityNames.length > 1
1172
- ? ' <span class="hint" title="Also declared by: ' + escapeHtml(v.abilityNames.join(', ')) + '. There is only one value for it project-wide, so check whether they actually need the same one.">(shared)</span>'
1173
- : '';
1174
- // A secret's value is never sent to the browser at all (see
1175
- // env-admin.ts) — "set" is the most this tab can ever say about one.
1176
- // A non-secret var's actual value comes through, so show it instead
1177
- // of a bare "set", since the whole point of looking here is usually
1178
- // to check *what* something is currently configured to, not just
1179
- // whether it has a value.
1180
- // An agent row (v.slot === 'agent' — this agent's own value, see
1181
- // env-admin.ts's DeclaredEnvVar.slot) for an overridable var isn't a
1182
- // problem when unset: it just falls back to the shared row. One for
1183
- // an agent-scoped var has no fallback, so unset really is missing.
1184
- var statusHtml = !v.set
1185
- ? (v.slot === 'agent' && v.scope === 'overridable'
1186
- ? '<span class="hint">not set &mdash; uses shared value</span>'
1187
- : '<span class="error">not set</span>')
1120
+ // One slot's cell: its current value (never a secret's), a form to set
1121
+ // it, and Remove once set. Slot "shared" is the project .env, slot
1122
+ // "agent" this agent's own agents/<name>/.env — see env-admin.ts's
1123
+ // DeclaredEnvVar.slot. The form/button classes and data-* attributes
1124
+ // are what wireEnvHandlers below binds to.
1125
+ function renderEnvSlotCell(v) {
1126
+ var status = !v.set
1127
+ ? (v.slot === 'agent' && v.scope === 'agent'
1128
+ ? '<span class="le-pill bad">missing</span>'
1129
+ : v.slot === 'agent'
1130
+ ? '<span class="hint">uses project-wide</span>'
1131
+ : '<span class="hint">not set</span>')
1188
1132
  : v.secret
1189
- ? '<span class="hint">set</span>'
1133
+ ? '<span class="le-pill good">set</span>'
1190
1134
  : '<code>' + escapeHtml(v.value || '') + '</code>';
1191
1135
  if (v.set && v.source === 'legacy') {
1192
- statusHtml += ' <span class="hint" title="Read from the older prefixed project variable. Saving a value here moves it into this agent&#39;s own .env.">(from ' + escapeHtml(v.legacyName || '') + ')</span>';
1136
+ status += ' <span class="hint" title="Read from the older prefixed project variable. Saving a value here moves it into this agent&#39;s own .env.">(from ' + escapeHtml(v.legacyName || '') + ')</span>';
1193
1137
  }
1194
- var slotNote = v.slot === 'agent'
1195
- ? ' <span class="hint">' + (v.scope === 'agent' ? 'this agent only' : 'this agent&#39;s override') + '</span>'
1196
- : (v.scope === 'overridable' ? ' <span class="hint">shared</span>' : '');
1197
- // A var with a closed set of valid values (AbilityEnvDecl.options,
1198
- // e.g. a provider switch) gets a <select> instead of a free-text
1199
- // field, so there's nothing to mistype — the same "value" form field
1200
- // name either element uses means wireEnvHandlers's own submit
1201
- // handler doesn't need to know which one it's looking at.
1202
- // A naturally multi-line value (AbilityEnvDecl.multiline, e.g. a
1203
- // pasted JSON key file) gets a <textarea> — a single-line <input>
1204
- // (even type="text") strips embedded line breaks from its value
1205
- // entirely per the HTML spec's own value sanitization, mangling
1206
- // exactly this kind of paste. No password-style masking for it
1207
- // either — <textarea> has no such mode, and masking a large blob
1208
- // secret buys little anyway compared to being able to paste it
1209
- // correctly at all.
1210
- var fieldHtml = v.options
1138
+ var field = v.options
1211
1139
  ? '<select name="value">' + v.options.map(function (opt) {
1212
1140
  return '<option value="' + escapeHtml(opt) + '"' + (v.value === opt ? ' selected' : '') + '>' + escapeHtml(opt) + '</option>';
1213
1141
  }).join('') + '</select>'
1214
1142
  : v.multiline
1215
- ? '<textarea name="value" rows="8" placeholder="' + (v.set ? 'unchanged unless you paste a new value' : 'paste the full value') + '" required></textarea>'
1216
- : '<input type="' + (v.secret ? 'password' : 'text') + '" name="value" placeholder="' + (v.set ? 'unchanged unless you type a new value' : 'value') + '" required>';
1217
- // Only offered once something is actually set — removing what's
1218
- // already absent has nothing to do, and the button would just be
1219
- // clutter for every var an operator hasn't touched yet (most of
1220
- // them, on a freshly installed ability — see renderEnvConfigHtml's
1221
- // own comment on why "not set" isn't itself treated as a problem).
1222
- var removeHtml = v.set
1223
- ? ' <button type="button" class="env-var-remove" data-name="' + escapeHtml(v.name) + '" data-slot="' + escapeHtml(v.slot) + '">Remove</button>'
1143
+ ? '<textarea name="value" rows="4" placeholder="' + (v.set ? 'paste a new value to replace it' : 'paste the full value') + '" required></textarea>'
1144
+ : '<input type="' + (v.secret ? 'password' : 'text') + '" name="value" placeholder="' + (v.set ? 'new value' : 'value') + '" required>';
1145
+ var remove = v.set
1146
+ ? '<button type="button" class="env-var-remove" data-name="' + escapeHtml(v.name) + '" data-slot="' + escapeHtml(v.slot) + '">Remove</button>'
1147
+ : '';
1148
+ return '<div class="env-cell"><div>' + status + '</div>' +
1149
+ '<form class="add-source env-var-form env-inline" data-name="' + escapeHtml(v.name) + '" data-slot="' + escapeHtml(v.slot) + '">' +
1150
+ field + '<button type="submit">' + (v.set ? 'Change' : 'Set') + '</button>' + remove +
1151
+ '</form></div>';
1152
+ }
1153
+
1154
+ var ENV_SCOPE_LABELS = {
1155
+ shared: 'Shared by every agent',
1156
+ overridable: 'Shared; an agent can set its own',
1157
+ agent: 'Set per agent; never shared',
1158
+ };
1159
+
1160
+ // One setting per row, its project-wide and per-agent values side by
1161
+ // side, plus which one this agent's tools actually read.
1162
+ function renderEnvSettingRow(shared, own) {
1163
+ var v = own || shared;
1164
+ var description = (shared && shared.description) || (own && own.description) || '';
1165
+ var alsoDeclared = v.abilityNames.length > 1
1166
+ ? ' <span class="hint" title="Also declared by: ' + escapeHtml(v.abilityNames.join(', ')) + '">(+' + (v.abilityNames.length - 1) + ' more)</span>'
1224
1167
  : '';
1168
+ var used = own && own.set
1169
+ ? 'this agent'
1170
+ : v.scope === 'agent'
1171
+ ? '<span class="error">nothing &mdash; its tools fail</span>'
1172
+ : shared && shared.set ? 'project-wide' : '<span class="hint">not set</span>';
1225
1173
  return '<tr>' +
1226
- '<td><code>' + escapeHtml(v.name) + '</code>' + slotNote + sharedNote + '</td>' +
1227
- '<td style="max-width:320px">' + escapeHtml(v.description || '') + '</td>' +
1228
- '<td>' + statusHtml + '</td>' +
1229
- '<td><form class="add-source env-var-form" data-name="' + escapeHtml(v.name) + '" data-slot="' + escapeHtml(v.slot) + '">' +
1230
- fieldHtml +
1231
- '<button type="submit">Save</button>' +
1232
- '</form>' + removeHtml + '</td>' +
1174
+ '<td style="min-width:200px"><code>' + escapeHtml(v.name) + '</code>' + (v.secret ? ' <span class="le-pill neutral">secret</span>' : '') + alsoDeclared +
1175
+ '<div class="hint">' + escapeHtml(ENV_SCOPE_LABELS[v.scope] || '') + '</div>' +
1176
+ (description ? '<div class="hint env-desc" title="' + escapeHtml(description) + '">' + escapeHtml(description) + '</div>' : '') + '</td>' +
1177
+ '<td>' + (shared ? renderEnvSlotCell(shared) : '<span class="hint">not used</span>') + '</td>' +
1178
+ '<td>' + (own ? renderEnvSlotCell(own) : '<span class="hint">&mdash;</span>') + '</td>' +
1179
+ '<td>' + used + '</td>' +
1233
1180
  '</tr>';
1234
1181
  }
1235
1182
 
1236
- // One card per ability, matching the same .source/.source-head look
1237
- // Gateway Tools sources already use — so an operator scanning the tab
1238
- // sees, per ability, which env vars it declared, instead of hunting
1239
- // through one long flat table for rows that belong to whichever
1240
- // ability they're actually trying to set up right now. Deliberately no
1241
- // "not set" count here (or in renderEnvConfigHtml's own summary) — a
1242
- // declared var not being set doesn't mean anything is broken, since
1243
- // plenty of them have working defaults (see each var's own
1244
- // description) and are only ever declared so an operator *can*
1245
- // override them, not because every single one is required.
1246
1183
  function renderEnvAbilityCard(abilityName, vars) {
1247
- var rows = vars.map(renderEnvRow).join('');
1248
- return '<div class="source">' +
1249
- '<div class="source-head"><h4>' + escapeHtml(abilityName) + '</h4><span class="hint">' + vars.length + ' var' + (vars.length === 1 ? '' : 's') + '</span></div>' +
1250
- '<table><thead><tr><th>Name</th><th style="max-width:320px">Description</th><th>Current value</th><th>Set value</th></tr></thead><tbody>' + rows + '</tbody></table>' +
1251
- '</div>';
1184
+ var names = [];
1185
+ var bySlot = {};
1186
+ vars.forEach(function (v) {
1187
+ if (!bySlot[v.name]) { bySlot[v.name] = {}; names.push(v.name); }
1188
+ bySlot[v.name][v.slot] = v;
1189
+ });
1190
+ var rows = names.map(function (n) { return renderEnvSettingRow(bySlot[n].shared, bySlot[n].agent); }).join('');
1191
+ return '<section><h3>' + escapeHtml(abilityName) + '</h3>' +
1192
+ '<div class="le-table-wrap"><table><thead><tr><th>Setting</th><th>Project-wide</th><th>This agent</th><th>Used</th></tr></thead><tbody>' + rows + '</tbody></table></div>' +
1193
+ '</section>';
1252
1194
  }
1253
1195
 
1254
1196
  function renderEnvConfigHtml(vars) {
1255
1197
  if (!vars.length) return '<p class="hint">No installed ability has declared any environment variables for this agent yet.</p>';
1256
1198
 
1257
- var summaryHtml = '<p class="hint">' + vars.length + ' environment variable' + (vars.length === 1 ? '' : 's') + ' across every installed ability.</p>';
1199
+ var summaryHtml = '<p class="muted" style="margin:0 0 20px">Settings your installed abilities read. A secret is never shown again after it is saved. Project-wide values live in the project <code>.env</code>; this agent&#39;s own values in <code>agents/&lt;name&gt;/.env</code>.</p>';
1258
1200
 
1259
1201
  // Grouped by declaring ability, not a single flat list — a var
1260
1202
  // declared by more than one ability (rare, but see renderEnvRow's
@@ -1904,14 +1846,15 @@ export const agentsConfigPageHtml = `<!doctype html>
1904
1846
  // describeGatewayTools), a real cost not worth paying up front. ----
1905
1847
 
1906
1848
  function renderToolsTabHtml(cfg) {
1907
- return '<section><h3>Local tools (' + cfg.localTools.length + ')</h3>' + renderLocalTools(cfg.localTools) +
1849
+ return '<p class="muted" style="margin:0 0 20px">Everything this agent can call. <strong>Runs</strong> comes from Permissions: allow runs on its own, ask waits for a person, deny never runs.</p>' +
1850
+ '<section><h3>Your tools (' + cfg.localTools.length + ')</h3>' + renderLocalTools(cfg.localTools) +
1908
1851
  '<div class="admin-subsection"><h4 id="httpToolFormHeading">Create an HTTP tool</h4>' + renderHttpToolFormHtml() + '</div>' +
1909
1852
  '</section>' +
1910
1853
  '<section id="gatewayToolsSection">' +
1911
- '<h3>Gateway Tools <button type="button" id="gatewayToolsRefreshBtn" class="hint-btn">Refresh</button></h3>' +
1854
+ '<h3>From integrations <button type="button" id="gatewayToolsRefreshBtn" class="hint-btn">Refresh</button></h3>' +
1912
1855
  '<div id="gatewayToolsContent"><p class="hint">Loading&hellip;</p></div>' +
1913
1856
  '</section>' +
1914
- '<section><h3>Agent as Tools (' + cfg.agentAsTools.length + ')</h3>' + renderTools(cfg.agentAsTools) + '</section>';
1857
+ '<section><h3>Subagents (' + cfg.agentAsTools.length + ')</h3>' + renderTools(cfg.agentAsTools) + '</section>';
1915
1858
  }
1916
1859
 
1917
1860
  // ---- Create an HTTP tool: generates a real agents/:name/tools/<tool>.ts
@@ -2480,12 +2423,17 @@ export const agentsConfigPageHtml = `<!doctype html>
2480
2423
 
2481
2424
  // ---- Tab switching + agent selection ----
2482
2425
 
2426
+ var CONFIG_TABS = ['overview', 'tools', 'skills', 'abilities', 'actauth', 'env'];
2427
+
2483
2428
  function switchTab(tab) {
2429
+ if (CONFIG_TABS.indexOf(tab) === -1) tab = 'overview';
2484
2430
  currentTab = tab;
2485
- var buttons = detail.querySelectorAll('.tabs button');
2486
- for (var i = 0; i < buttons.length; i++) {
2487
- buttons[i].classList.toggle('active', buttons[i].dataset.tab === tab);
2488
- }
2431
+ leShell.setTab(tab);
2432
+ // Same URL a fresh load of this tab would have, so reload, back/forward
2433
+ // and a copied link all land here — without a navigation per click.
2434
+ var url = new URL(location.href);
2435
+ if (tab === 'overview') url.searchParams.delete('tab'); else url.searchParams.set('tab', tab);
2436
+ history.replaceState(null, '', url.pathname + url.search);
2489
2437
  var panels = detail.querySelectorAll('.tab-panel');
2490
2438
  for (var i = 0; i < panels.length; i++) {
2491
2439
  panels[i].style.display = panels[i].dataset.tabPanel === tab ? 'block' : 'none';
@@ -2576,30 +2524,29 @@ export const agentsConfigPageHtml = `<!doctype html>
2576
2524
  .catch(function () {});
2577
2525
  }
2578
2526
 
2527
+ function modelChip(cfg) {
2528
+ if (typeof cfg.model === 'string') return { text: cfg.model, mono: true };
2529
+ return { text: cfg.model.provider + ' \u00b7 ' + (cfg.model.model || 'default model'), mono: true };
2530
+ }
2531
+
2579
2532
  function renderDetail(cfg) {
2580
2533
  currentCfg = cfg;
2581
2534
  detail.innerHTML =
2582
- '<h2>' + escapeHtml(cfg.name) +
2583
- ' <a id="playgroundLink" href="/playground?agent=' + encodeURIComponent(cfg.name) + '">Open in playground &rarr;</a></h2>' +
2584
- '<div class="tabs">' +
2585
- '<button class="tab" data-tab="overview">Overview</button>' +
2586
- '<button class="tab" data-tab="skills">Skills</button>' +
2587
- '<button class="tab" data-tab="tools">Tools</button>' +
2588
- '<button class="tab" data-tab="actauth">ActAuth</button>' +
2589
- '<button class="tab" data-tab="abilities">Abilities</button>' +
2590
- '<button class="tab" data-tab="env">Environment</button>' +
2591
- '</div>' +
2535
+ leShell.renderHead({
2536
+ name: cfg.name,
2537
+ prompt: cfg.systemPrompt,
2538
+ chips: [modelChip(cfg), { text: 'unmatched calls: ' + cfg.permissions.defaultDecision }],
2539
+ tab: currentTab,
2540
+ counts: { tools: cfg.tools.length, skills: cfg.skills.length, actauth: cfg.permissions.rules.length },
2541
+ }) +
2542
+ '<div class="le-scroll"><div class="le-page" style="display:block">' +
2592
2543
  '<div class="tab-panel" data-tab-panel="overview">' + renderOverviewHtml(cfg) + '</div>' +
2593
2544
  '<div class="tab-panel" data-tab-panel="skills">' + renderSkillsTabHtml(cfg) + '</div>' +
2594
2545
  '<div class="tab-panel" data-tab-panel="tools">' + renderToolsTabHtml(cfg) + '</div>' +
2595
2546
  '<div class="tab-panel" data-tab-panel="actauth">' + renderActauthTabPlaceholder() + '</div>' +
2596
2547
  '<div class="tab-panel" data-tab-panel="env">' + renderEnvTabPlaceholder() + '</div>' +
2597
- '<div class="tab-panel" data-tab-panel="abilities">' + renderAbilitiesTabPlaceholder() + '</div>';
2598
-
2599
- var buttons = detail.querySelectorAll('.tabs button');
2600
- for (var i = 0; i < buttons.length; i++) {
2601
- buttons[i].addEventListener('click', function (ev) { switchTab(ev.currentTarget.dataset.tab); });
2602
- }
2548
+ '<div class="tab-panel" data-tab-panel="abilities">' + renderAbilitiesTabPlaceholder() + '</div>' +
2549
+ '</div></div>';
2603
2550
 
2604
2551
  wireSkillsHandlers(cfg.name);
2605
2552
  wireOverviewHandlers(cfg.name);
@@ -2610,7 +2557,7 @@ export const agentsConfigPageHtml = `<!doctype html>
2610
2557
  }
2611
2558
 
2612
2559
  empty.style.display = 'none';
2613
- detail.style.display = 'block';
2560
+ detail.style.display = 'flex';
2614
2561
  switchTab(currentTab);
2615
2562
  }
2616
2563
 
@@ -2620,13 +2567,10 @@ export const agentsConfigPageHtml = `<!doctype html>
2620
2567
  actauthLoadedFor = null;
2621
2568
  envLoadedFor = null;
2622
2569
  abilitiesLoadedFor = null;
2623
- var items = agentList.querySelectorAll('li');
2624
- for (var i = 0; i < items.length; i++) {
2625
- items[i].classList.toggle('active', items[i].dataset.name === name);
2626
- }
2627
- detail.innerHTML = '<p class="muted">Loading&hellip;</p>';
2570
+ leShell.setActive(name);
2571
+ detail.innerHTML = '<p class="muted" style="padding:24px">Loading&hellip;</p>';
2628
2572
  empty.style.display = 'none';
2629
- detail.style.display = 'block';
2573
+ detail.style.display = 'flex';
2630
2574
  fetch('/agents/' + encodeURIComponent(name) + '/config')
2631
2575
  .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
2632
2576
  .then(function (result) {
@@ -2647,37 +2591,27 @@ export const agentsConfigPageHtml = `<!doctype html>
2647
2591
  });
2648
2592
  }
2649
2593
 
2650
- fetch('/agents')
2651
- .then(function (r) { return r.json(); })
2652
- .then(function (data) {
2653
- var agents = data.agents || [];
2654
- agentList.textContent = '';
2655
- for (var i = 0; i < agents.length; i++) {
2656
- (function (name) {
2657
- var li = document.createElement('li');
2658
- li.dataset.name = name;
2659
- // A real <a href>, not a click handler calling selectAgent —
2660
- // switching agents is a full navigation to this same page with
2661
- // a different ?agent=, so the URL, browser back/forward, and
2662
- // bookmarking all reflect which agent is open, instead of the
2663
- // URL staying frozen at whatever it was on first load while
2664
- // the panel silently swaps underneath it via fetch.
2665
- var a = document.createElement('a');
2666
- a.href = '/agents/config?agent=' + encodeURIComponent(name);
2667
- a.textContent = name;
2668
- li.appendChild(a);
2669
- agentList.appendChild(li);
2670
- })(agents[i].name);
2671
- }
2672
- if (agents.length) {
2673
- var requested = new URLSearchParams(location.search).get('agent');
2674
- var initial = agents.some(function (a) { return a.name === requested; }) ? requested : agents[0].name;
2675
- selectAgent(initial);
2676
- }
2677
- })
2678
- .catch(function (err) {
2679
- empty.textContent = 'Could not load agents: ' + err.message;
2680
- });
2594
+ // Chat is a real link to the playground; every other tab, and every
2595
+ // Overview card pointing at one, switches in place. Delegated once
2596
+ // here because Overview's cards are re-rendered after every edit.
2597
+ detail.addEventListener('click', function (ev) {
2598
+ var link = ev.target.closest('a[data-tab]');
2599
+ if (!link || link.dataset.tab === 'chat' || ev.metaKey || ev.ctrlKey || ev.shiftKey) return;
2600
+ ev.preventDefault();
2601
+ switchTab(link.dataset.tab);
2602
+ });
2603
+
2604
+ var initialParams = new URLSearchParams(location.search);
2605
+ currentTab = CONFIG_TABS.indexOf(initialParams.get('tab')) === -1 ? 'overview' : initialParams.get('tab');
2606
+ leShell.ready.then(function (agents) {
2607
+ if (!agents.length) {
2608
+ empty.innerHTML = 'No agents yet. <a href="/agents?new=1">Create one</a> or add a folder under agents/.';
2609
+ return;
2610
+ }
2611
+ var requested = initialParams.get('agent');
2612
+ var initial = agents.some(function (a) { return a.name === requested; }) ? requested : agents[0].name;
2613
+ selectAgent(initial);
2614
+ });
2681
2615
  })();
2682
2616
  </script>
2683
2617
  </body>