loopengine 0.1.63 → 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.
@@ -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
- main { max-width: 640px; margin: 0 auto; padding: 24px 20px; }
17
- h1 { font-size: 18px; margin: 0 0 4px; }
18
- p.intro { color: light-dark(#666, #999); font-size: 13px; margin: 0 0 20px; }
19
- ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 10px; }
20
- li {
21
- border: 1px solid light-dark(#ddd, #333);
22
- border-radius: 8px;
23
- padding: 12px 14px;
24
- background: light-dark(#fff, #202024);
25
- display: flex;
26
- align-items: center;
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
- display: flex;
55
- flex-direction: column;
56
- gap: 8px;
57
- margin: 4px 0 20px;
58
- max-width: 480px;
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 details textarea { min-height: 60px; font-family: inherit; }
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
- <nav class="topnav">
76
- <a href="/agents" class="active">Agents</a>
77
- <a href="/agents/config">Config</a>
78
- <a href="/playground">Playground</a>
79
- </nav>
80
- <main>
81
- <div class="page-head">
82
- <h1>Registered agents</h1>
83
- <button type="button" id="newAgentBtn">+ New agent</button>
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">&#9776;</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
- <details>
93
- <summary>Advanced (optional)</summary>
94
- <label>System prompt
95
- <textarea name="systemPrompt" placeholder="You are ..."></textarea>
96
- </label>
97
- <label>Model provider
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/&lt;name&gt;/)</span>
60
+ <input type="text" name="name" placeholder="weather-agent" pattern="[a-z0-9]+(-[a-z0-9]+)*" required>
106
61
  </label>
107
- </details>
108
- </form>
109
- <p class="error" id="newAgentError" style="display:none"></p>
110
- <p class="hint" id="newAgentResult" style="display:none"></p>
111
- <ul id="agentUl"></ul>
112
- <p id="empty" style="display:none">No agents registered.</p>
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><div class="name">' + escapeHtml(agent.name) + '</div>' +
149
- '<div class="prompt">' + escapeHtml(agent.systemPrompt) + '</div></div>' +
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="/agents/config' + qs + '">Config</a>' +
152
- '<a href="/playground' + qs + '">Playground</a>' +
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 = 'flex';
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
- newAgentResult.textContent = 'Created ' + result.body.path + ' — it\\'s live now.';
224
- loadAgents();
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,2 @@
1
+ export declare const consoleSidebarHtml: string;
2
+ export declare const consoleShellScript: string;
@@ -0,0 +1,156 @@
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'], ['tools', 'Tools'], ['skills', 'Skills'],
45
+ ['abilities', 'Abilities'], ['actauth', 'Permissions'], ['env', 'Environment']
46
+ ];
47
+
48
+ function esc(s) {
49
+ return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
50
+ return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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">&#9776;</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
+ function setActive(name) {
138
+ activeAgent = name;
139
+ renderList();
140
+ }
141
+
142
+ var foot = page === 'config' ? (params.get('section') === 'gateways' ? 'integrations' : 'models') : '';
143
+ var footLinks = document.querySelectorAll('[data-le-nav]');
144
+ for (var i = 0; i < footLinks.length; i++) {
145
+ if (footLinks[i].getAttribute('data-le-nav') === foot) footLinks[i].setAttribute('aria-current', 'page');
146
+ }
147
+
148
+ document.addEventListener('click', function (ev) {
149
+ if (!app) return;
150
+ if (ev.target.closest('[data-le-menu]')) { app.classList.toggle('nav-open'); return; }
151
+ if (app.classList.contains('nav-open') && !ev.target.closest('.le-sidebar')) app.classList.remove('nav-open');
152
+ });
153
+
154
+ window.leShell = { ready: ready, renderHead: renderHead, setTab: setTab, setActive: setActive, esc: esc, tabHref: tabHref, envCountHtml: envCountHtml };
155
+ })();
156
+ </script>`;
@@ -1,63 +1,169 @@
1
- // Shared look for every browser page adapters/http.ts serves — playground
2
- // (playground.ts), the per-agent config viewer (agents-config-page.ts),
3
- // and the agents index (agents-list-page.ts). Pulled out once these grew to
4
- // three pages that all want the same fonts/colors/form controls: without
5
- // this, harmonizing them meant hand-syncing the same CSS block in three
6
- // separate template-literal strings every time one changed. Kept to the
7
- // tokens actually shared across all three (base colors, form controls,
8
- // badges, the cross-page nav strip) — each page still owns its own
9
- // page-specific rules (chat bubbles, timeline events, permission tables, ...)
10
- // in its own <style> block.
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 { color-scheme: light dark; }
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: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
17
- background: light-dark(#f7f7f8, #1a1a1e);
18
- color: light-dark(#1a1a1e, #e8e8ea);
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: light-dark(#1d4ed8, #60a5fa); }
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: light-dark(#fff, #26262b);
25
- border: 1px solid light-dark(#ccc, #444);
63
+ background: var(--bg);
64
+ border: 1px solid var(--line);
26
65
  border-radius: 6px;
27
- padding: 6px 8px;
66
+ padding: 6px 10px;
28
67
  }
68
+ textarea { line-height: 1.45; }
29
69
  button {
30
70
  cursor: pointer;
31
- background: light-dark(#e8e8ea, #333338);
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: ui-monospace, monospace;
36
- background: light-dark(#eee, #2a2a2e);
82
+ font-family: var(--font-mono);
83
+ background: var(--code-bg);
37
84
  padding: 1px 5px;
38
85
  border-radius: 4px;
39
- font-size: 11px;
86
+ font-size: 12px;
40
87
  }
41
88
  .badge {
42
89
  display: inline-block;
43
90
  font-size: 11px;
44
- font-family: ui-monospace, monospace;
45
- padding: 2px 8px;
46
- border-radius: 10px;
47
- background: light-dark(#eee, #2a2a2e);
48
- }
49
- .badge-custom { background: light-dark(#dcfce7, #14532d); color: light-dark(#166534, #86efac); }
50
- .badge-default { background: light-dark(#eee, #2a2a2e); color: light-dark(#666, #999); }
51
- .muted { color: light-dark(#666, #999); font-size: 12px; }
52
- .topnav {
53
- display: flex;
54
- gap: 14px;
55
- padding: 8px 16px;
56
- font-size: 12px;
57
- border-bottom: 1px solid light-dark(#ddd, #333);
58
- background: light-dark(#fff, #202024);
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
- .topnav a { text-decoration: none; }
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
  `;
@@ -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
- if (scope === 'overridable') {
108
- upsert('agent', abilityName, decl, `This agent's own value — if unset, falls back to the shared value.${decl.description ? ` ${decl.description}` : ''}`);
109
- }
110
- else if (scope === 'agent') {
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