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.
@@ -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,36 @@ 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
+ (cfg.abilityCount != null
604
+ ? card('abilities', 'Abilities', String(cfg.abilityCount), cfg.abilityCount ? 'Installed for this agent' : 'Add tools, skills and rules in one install')
605
+ : card('abilities', 'Abilities', 'Browse', 'Add tools, skills and rules in one install')) +
606
+ card('tools', 'Tools', String(cfg.tools.length), escapeHtml(toolNames.slice(0, 3).join(', ') + (toolNames.length > 3 ? ', \u2026' : '')) || 'None yet') +
607
+ card('skills', 'Skills', String(cfg.skills.length), escapeHtml(cfg.skills.map(function (k) { return k.name; }).join(', ')) || 'None yet') +
608
+ card('actauth', 'Permissions', rules.length + (rules.length === 1 ? ' rule' : ' rules'),
609
+ (asks ? asks + ' need approval \u00b7 ' : '') + 'unmatched calls: ' + escapeHtml(cfg.permissions.defaultDecision)) +
610
+ card('env', 'Environment', 'Settings', '<span data-le-env-count="' + escapeHtml(cfg.name) + '">' + leShell.envCountHtml(cfg.name) + '</span> Values your installed abilities read') +
611
+ '</div>';
612
+ }
613
+
651
614
  function renderOverviewHtml(cfg) {
652
615
  var systemPromptSection =
653
616
  '<h3>System prompt <button type="button" class="edit-btn" id="editSystemPromptBtn">Edit</button></h3>' +
@@ -703,11 +666,9 @@ export const agentsConfigPageHtml = `<!doctype html>
703
666
  '</form>';
704
667
  }
705
668
 
706
- return '<section>' + systemPromptSection + '</section>' +
669
+ return renderOverviewCards(cfg) +
670
+ '<section>' + systemPromptSection + '</section>' +
707
671
  '<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
672
  '<section><h3>Hooks</h3><dl class="kv">' +
712
673
  '<dt>sessionIdFor</dt><dd>' + badge(cfg.sessionIdFor.indexOf('custom') === 0 ? 'custom' : 'default') + ' <span class="muted">' + escapeHtml(cfg.sessionIdFor) + '</span></dd>' +
713
674
  '<dt>tenantFor</dt><dd>' + badge(cfg.tenantFor.indexOf('custom') === 0 ? 'custom' : 'default') + ' <span class="muted">' + escapeHtml(cfg.tenantFor) + '</span></dd>' +
@@ -943,20 +904,22 @@ export const agentsConfigPageHtml = `<!doctype html>
943
904
  '<td><code>' + escapeHtml(r.name || '') + '</code></td>' +
944
905
  '<td><code>' + escapeHtml(r.scope) + '</code></td>' +
945
906
  '<td><code>' + escapeHtml(r.tool) + '</code></td>' +
946
- '<td class="' + decisionClass(r.decision) + '">' + escapeHtml(r.decision) + '</td>' +
907
+ '<td><span class="le-pill ' + escapeHtml(r.decision) + '">' + escapeHtml(r.decision) + '</span></td>' +
947
908
  '<td>' + actionsHtml + '</td>' +
948
909
  '</tr>';
949
910
  }
950
911
 
951
912
  function renderActauthConfigHtml(cfg) {
952
913
  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>' +
914
+ ? '<table><thead><tr><th>Name</th><th>Who (tenant / environment)</th><th>Tool</th><th>Decision</th><th></th></tr></thead><tbody>' +
954
915
  cfg.rules.map(renderActauthRuleRow).join('') + '</tbody></table>'
955
916
  : '<p class="hint">No explicit rules — every tool call falls through to the default decision.</p>';
956
917
 
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' +
918
+ 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>' +
919
+ '<section><h3>Rules (' + cfg.rules.length + ')</h3>' + rulesHtml + '</section>' +
920
+ '<section><h3>When no rule matches</h3>' +
921
+ '<form class="add-source" id="defaultDecisionForm" style="max-width:260px">' +
922
+ '<label>Decision' +
960
923
  '<select name="defaultDecision" id="defaultDecisionSelect">' +
961
924
  '<option value="allow"' + (cfg.defaultDecision === 'allow' ? ' selected' : '') + '>allow</option>' +
962
925
  '<option value="ask"' + (cfg.defaultDecision === 'ask' ? ' selected' : '') + '>ask</option>' +
@@ -966,13 +929,12 @@ export const agentsConfigPageHtml = `<!doctype html>
966
929
  '<button type="submit">Save</button>' +
967
930
  '<div id="defaultDecisionError" class="error"></div>' +
968
931
  '</form></section>' +
969
- '<section><h3>Rules (' + cfg.rules.length + ')</h3>' + rulesHtml + '</section>' +
970
932
  '<section><h3 id="ruleFormHeading">Add a rule</h3>' +
971
933
  '<form class="add-source" id="ruleForm">' +
972
934
  '<label>Name' +
973
935
  '<input name="name" id="ruleNameInput" required placeholder="my-rule-name">' +
974
936
  '</label>' +
975
- '<label>Scope <span class="hint">(tenant/environment — e.g. "default/production" or "*/*"; the agent segment is appended automatically)</span>' +
937
+ '<label>Who <span class="hint">(tenant/environment, e.g. "default/production" or "*/*" for everyone)</span>' +
976
938
  '<input name="scope" id="ruleScopeInput" required placeholder="default/production">' +
977
939
  '</label>' +
978
940
  '<label>Tool' +
@@ -1157,104 +1119,86 @@ export const agentsConfigPageHtml = `<!doctype html>
1157
1119
  return '<section id="envSection"><div id="envContent"><p class="hint">Loading&hellip;</p></div></section>';
1158
1120
  }
1159
1121
 
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>')
1122
+ // One slot's cell: its current value (never a secret's), a form to set
1123
+ // it, and Remove once set. Slot "shared" is the project .env, slot
1124
+ // "agent" this agent's own agents/<name>/.env — see env-admin.ts's
1125
+ // DeclaredEnvVar.slot. The form/button classes and data-* attributes
1126
+ // are what wireEnvHandlers below binds to.
1127
+ function renderEnvSlotCell(v) {
1128
+ var status = !v.set
1129
+ ? (v.slot === 'agent' && v.scope === 'agent'
1130
+ ? '<span class="le-pill bad">missing</span>'
1131
+ : v.slot === 'agent'
1132
+ ? '<span class="hint">uses project-wide</span>'
1133
+ : '<span class="hint">not set</span>')
1188
1134
  : v.secret
1189
- ? '<span class="hint">set</span>'
1135
+ ? '<span class="le-pill good">set</span>'
1190
1136
  : '<code>' + escapeHtml(v.value || '') + '</code>';
1191
1137
  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>';
1138
+ 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
1139
  }
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
1140
+ var field = v.options
1211
1141
  ? '<select name="value">' + v.options.map(function (opt) {
1212
1142
  return '<option value="' + escapeHtml(opt) + '"' + (v.value === opt ? ' selected' : '') + '>' + escapeHtml(opt) + '</option>';
1213
1143
  }).join('') + '</select>'
1214
1144
  : 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>'
1145
+ ? '<textarea name="value" rows="4" placeholder="' + (v.set ? 'paste a new value to replace it' : 'paste the full value') + '" required></textarea>'
1146
+ : '<input type="' + (v.secret ? 'password' : 'text') + '" name="value" placeholder="' + (v.set ? 'new value' : 'value') + '" required>';
1147
+ var remove = v.set
1148
+ ? '<button type="button" class="env-var-remove" data-name="' + escapeHtml(v.name) + '" data-slot="' + escapeHtml(v.slot) + '">Remove</button>'
1149
+ : '';
1150
+ return '<div class="env-cell"><div>' + status + '</div>' +
1151
+ '<form class="add-source env-var-form env-inline" data-name="' + escapeHtml(v.name) + '" data-slot="' + escapeHtml(v.slot) + '">' +
1152
+ field + '<button type="submit">' + (v.set ? 'Change' : 'Set') + '</button>' + remove +
1153
+ '</form></div>';
1154
+ }
1155
+
1156
+ var ENV_SCOPE_LABELS = {
1157
+ shared: 'Shared by every agent',
1158
+ overridable: 'Shared; an agent can set its own',
1159
+ agent: 'Set per agent; never shared',
1160
+ };
1161
+
1162
+ // One setting per row, its project-wide and per-agent values side by
1163
+ // side, plus which one this agent's tools actually read.
1164
+ function renderEnvSettingRow(shared, own) {
1165
+ var v = own || shared;
1166
+ var description = (shared && shared.description) || (own && own.description) || '';
1167
+ var alsoDeclared = v.abilityNames.length > 1
1168
+ ? ' <span class="hint" title="Also declared by: ' + escapeHtml(v.abilityNames.join(', ')) + '">(+' + (v.abilityNames.length - 1) + ' more)</span>'
1224
1169
  : '';
1170
+ var used = own && own.set
1171
+ ? 'this agent'
1172
+ : v.scope === 'agent'
1173
+ ? '<span class="error">nothing &mdash; its tools fail</span>'
1174
+ : shared && shared.set ? 'project-wide' : '<span class="hint">not set</span>';
1225
1175
  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>' +
1176
+ '<td style="min-width:200px"><code>' + escapeHtml(v.name) + '</code>' + (v.secret ? ' <span class="le-pill neutral">secret</span>' : '') + alsoDeclared +
1177
+ '<div class="hint">' + escapeHtml(ENV_SCOPE_LABELS[v.scope] || '') + '</div>' +
1178
+ (description ? '<div class="hint env-desc" title="' + escapeHtml(description) + '">' + escapeHtml(description) + '</div>' : '') + '</td>' +
1179
+ '<td>' + (shared ? renderEnvSlotCell(shared) : '<span class="hint">not used</span>') + '</td>' +
1180
+ '<td>' + (own ? renderEnvSlotCell(own) : '<span class="hint">&mdash;</span>') + '</td>' +
1181
+ '<td>' + used + '</td>' +
1233
1182
  '</tr>';
1234
1183
  }
1235
1184
 
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
1185
  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>';
1186
+ var names = [];
1187
+ var bySlot = {};
1188
+ vars.forEach(function (v) {
1189
+ if (!bySlot[v.name]) { bySlot[v.name] = {}; names.push(v.name); }
1190
+ bySlot[v.name][v.slot] = v;
1191
+ });
1192
+ var rows = names.map(function (n) { return renderEnvSettingRow(bySlot[n].shared, bySlot[n].agent); }).join('');
1193
+ return '<section><h3>' + escapeHtml(abilityName) + '</h3>' +
1194
+ '<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>' +
1195
+ '</section>';
1252
1196
  }
1253
1197
 
1254
1198
  function renderEnvConfigHtml(vars) {
1255
1199
  if (!vars.length) return '<p class="hint">No installed ability has declared any environment variables for this agent yet.</p>';
1256
1200
 
1257
- var summaryHtml = '<p class="hint">' + vars.length + ' environment variable' + (vars.length === 1 ? '' : 's') + ' across every installed ability.</p>';
1201
+ 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
1202
 
1259
1203
  // Grouped by declaring ability, not a single flat list — a var
1260
1204
  // declared by more than one ability (rare, but see renderEnvRow's
@@ -1692,6 +1636,8 @@ export const agentsConfigPageHtml = `<!doctype html>
1692
1636
  if (!content) return;
1693
1637
  content.innerHTML = renderAbilitiesConfigHtml(data);
1694
1638
  var installedNames = (data.abilities || []).map(function (a) { return a.name; });
1639
+ // Keeps the tab badge right after an install or uninstall here.
1640
+ leShell.setCount('abilities', installedNames.length);
1695
1641
  wireAbilitiesHandlers(name, installedNames);
1696
1642
  abilitiesLoadedFor = name;
1697
1643
  }
@@ -1904,14 +1850,15 @@ export const agentsConfigPageHtml = `<!doctype html>
1904
1850
  // describeGatewayTools), a real cost not worth paying up front. ----
1905
1851
 
1906
1852
  function renderToolsTabHtml(cfg) {
1907
- return '<section><h3>Local tools (' + cfg.localTools.length + ')</h3>' + renderLocalTools(cfg.localTools) +
1853
+ 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>' +
1854
+ '<section><h3>Your tools (' + cfg.localTools.length + ')</h3>' + renderLocalTools(cfg.localTools) +
1908
1855
  '<div class="admin-subsection"><h4 id="httpToolFormHeading">Create an HTTP tool</h4>' + renderHttpToolFormHtml() + '</div>' +
1909
1856
  '</section>' +
1910
1857
  '<section id="gatewayToolsSection">' +
1911
- '<h3>Gateway Tools <button type="button" id="gatewayToolsRefreshBtn" class="hint-btn">Refresh</button></h3>' +
1858
+ '<h3>From integrations <button type="button" id="gatewayToolsRefreshBtn" class="hint-btn">Refresh</button></h3>' +
1912
1859
  '<div id="gatewayToolsContent"><p class="hint">Loading&hellip;</p></div>' +
1913
1860
  '</section>' +
1914
- '<section><h3>Agent as Tools (' + cfg.agentAsTools.length + ')</h3>' + renderTools(cfg.agentAsTools) + '</section>';
1861
+ '<section><h3>Subagents (' + cfg.agentAsTools.length + ')</h3>' + renderTools(cfg.agentAsTools) + '</section>';
1915
1862
  }
1916
1863
 
1917
1864
  // ---- Create an HTTP tool: generates a real agents/:name/tools/<tool>.ts
@@ -2480,12 +2427,17 @@ export const agentsConfigPageHtml = `<!doctype html>
2480
2427
 
2481
2428
  // ---- Tab switching + agent selection ----
2482
2429
 
2430
+ var CONFIG_TABS = ['overview', 'tools', 'skills', 'abilities', 'actauth', 'env'];
2431
+
2483
2432
  function switchTab(tab) {
2433
+ if (CONFIG_TABS.indexOf(tab) === -1) tab = 'overview';
2484
2434
  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
- }
2435
+ leShell.setTab(tab);
2436
+ // Same URL a fresh load of this tab would have, so reload, back/forward
2437
+ // and a copied link all land here — without a navigation per click.
2438
+ var url = new URL(location.href);
2439
+ if (tab === 'overview') url.searchParams.delete('tab'); else url.searchParams.set('tab', tab);
2440
+ history.replaceState(null, '', url.pathname + url.search);
2489
2441
  var panels = detail.querySelectorAll('.tab-panel');
2490
2442
  for (var i = 0; i < panels.length; i++) {
2491
2443
  panels[i].style.display = panels[i].dataset.tabPanel === tab ? 'block' : 'none';
@@ -2576,30 +2528,29 @@ export const agentsConfigPageHtml = `<!doctype html>
2576
2528
  .catch(function () {});
2577
2529
  }
2578
2530
 
2531
+ function modelChip(cfg) {
2532
+ if (typeof cfg.model === 'string') return { text: cfg.model, mono: true };
2533
+ return { text: cfg.model.provider + ' \u00b7 ' + (cfg.model.model || 'default model'), mono: true };
2534
+ }
2535
+
2579
2536
  function renderDetail(cfg) {
2580
2537
  currentCfg = cfg;
2581
2538
  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>' +
2539
+ leShell.renderHead({
2540
+ name: cfg.name,
2541
+ prompt: cfg.systemPrompt,
2542
+ chips: [modelChip(cfg), { text: 'unmatched calls: ' + cfg.permissions.defaultDecision }],
2543
+ tab: currentTab,
2544
+ counts: { tools: cfg.tools.length, skills: cfg.skills.length, abilities: cfg.abilityCount, actauth: cfg.permissions.rules.length },
2545
+ }) +
2546
+ '<div class="le-scroll"><div class="le-page" style="display:block">' +
2592
2547
  '<div class="tab-panel" data-tab-panel="overview">' + renderOverviewHtml(cfg) + '</div>' +
2593
2548
  '<div class="tab-panel" data-tab-panel="skills">' + renderSkillsTabHtml(cfg) + '</div>' +
2594
2549
  '<div class="tab-panel" data-tab-panel="tools">' + renderToolsTabHtml(cfg) + '</div>' +
2595
2550
  '<div class="tab-panel" data-tab-panel="actauth">' + renderActauthTabPlaceholder() + '</div>' +
2596
2551
  '<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
- }
2552
+ '<div class="tab-panel" data-tab-panel="abilities">' + renderAbilitiesTabPlaceholder() + '</div>' +
2553
+ '</div></div>';
2603
2554
 
2604
2555
  wireSkillsHandlers(cfg.name);
2605
2556
  wireOverviewHandlers(cfg.name);
@@ -2610,7 +2561,7 @@ export const agentsConfigPageHtml = `<!doctype html>
2610
2561
  }
2611
2562
 
2612
2563
  empty.style.display = 'none';
2613
- detail.style.display = 'block';
2564
+ detail.style.display = 'flex';
2614
2565
  switchTab(currentTab);
2615
2566
  }
2616
2567
 
@@ -2620,13 +2571,10 @@ export const agentsConfigPageHtml = `<!doctype html>
2620
2571
  actauthLoadedFor = null;
2621
2572
  envLoadedFor = null;
2622
2573
  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>';
2574
+ leShell.setActive(name);
2575
+ detail.innerHTML = '<p class="muted" style="padding:24px">Loading&hellip;</p>';
2628
2576
  empty.style.display = 'none';
2629
- detail.style.display = 'block';
2577
+ detail.style.display = 'flex';
2630
2578
  fetch('/agents/' + encodeURIComponent(name) + '/config')
2631
2579
  .then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
2632
2580
  .then(function (result) {
@@ -2647,37 +2595,27 @@ export const agentsConfigPageHtml = `<!doctype html>
2647
2595
  });
2648
2596
  }
2649
2597
 
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
- });
2598
+ // Chat is a real link to the playground; every other tab, and every
2599
+ // Overview card pointing at one, switches in place. Delegated once
2600
+ // here because Overview's cards are re-rendered after every edit.
2601
+ detail.addEventListener('click', function (ev) {
2602
+ var link = ev.target.closest('a[data-tab]');
2603
+ if (!link || link.dataset.tab === 'chat' || ev.metaKey || ev.ctrlKey || ev.shiftKey) return;
2604
+ ev.preventDefault();
2605
+ switchTab(link.dataset.tab);
2606
+ });
2607
+
2608
+ var initialParams = new URLSearchParams(location.search);
2609
+ currentTab = CONFIG_TABS.indexOf(initialParams.get('tab')) === -1 ? 'overview' : initialParams.get('tab');
2610
+ leShell.ready.then(function (agents) {
2611
+ if (!agents.length) {
2612
+ empty.innerHTML = 'No agents yet. <a href="/agents?new=1">Create one</a> or add a folder under agents/.';
2613
+ return;
2614
+ }
2615
+ var requested = initialParams.get('agent');
2616
+ var initial = agents.some(function (a) { return a.name === requested; }) ? requested : agents[0].name;
2617
+ selectAgent(initial);
2618
+ });
2681
2619
  })();
2682
2620
  </script>
2683
2621
  </body>