aiden-runtime 3.16.2 → 3.18.0

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.
@@ -0,0 +1,480 @@
1
+ "use strict";
2
+ // api/dashboard.ts — Aiden local web dashboard
3
+ // Served at GET /ui — single self-contained HTML, no external build step.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.getDashboardHTML = getDashboardHTML;
6
+ function getDashboardHTML() {
7
+ return `<!DOCTYPE html>
8
+ <html lang="en">
9
+ <head>
10
+ <meta charset="UTF-8"/>
11
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
12
+ <title>Aiden Dashboard</title>
13
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
14
+ <style>
15
+ :root {
16
+ --bg: #0D0D0D;
17
+ --surface: #1A1A1A;
18
+ --surface2: #222222;
19
+ --border: #2A2A2A;
20
+ --orange: #FF6B35;
21
+ --orange-dim: #c0521e;
22
+ --text: #E8E8E8;
23
+ --text-dim: #888;
24
+ --green: #4CAF50;
25
+ --red: #F44336;
26
+ --yellow: #FFC107;
27
+ --radius: 8px;
28
+ --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
29
+ }
30
+ *{box-sizing:border-box;margin:0;padding:0}
31
+ body{background:var(--bg);color:var(--text);font-family:var(--font);font-size:14px;height:100vh;display:flex;flex-direction:column;overflow:hidden}
32
+
33
+ /* ── Top bar ── */
34
+ #topbar{display:flex;align-items:center;gap:12px;padding:0 20px;height:48px;border-bottom:1px solid var(--border);background:var(--surface);flex-shrink:0}
35
+ #logo{font-weight:700;font-size:16px;color:var(--orange);letter-spacing:.5px}
36
+ #status-dot{width:8px;height:8px;border-radius:50%;background:var(--red);transition:background .4s}
37
+ #status-dot.ok{background:var(--green)}
38
+ #status-text{color:var(--text-dim);font-size:12px}
39
+ #topbar-right{margin-left:auto;display:flex;gap:8px;align-items:center}
40
+ #version-label{color:var(--text-dim);font-size:11px}
41
+
42
+ /* ── Tab bar ── */
43
+ #tabbar{display:flex;gap:2px;padding:8px 20px 0;background:var(--surface);border-bottom:1px solid var(--border);flex-shrink:0}
44
+ .tab{padding:8px 18px;border-radius:6px 6px 0 0;cursor:pointer;font-size:13px;font-weight:500;color:var(--text-dim);border:1px solid transparent;border-bottom:none;background:transparent;transition:all .15s}
45
+ .tab:hover{color:var(--text);background:var(--surface2)}
46
+ .tab.active{color:var(--orange);background:var(--bg);border-color:var(--border)}
47
+
48
+ /* ── Main content ── */
49
+ #panels{flex:1;overflow:hidden;display:flex;flex-direction:column}
50
+ .panel{display:none;flex:1;overflow:hidden;flex-direction:column}
51
+ .panel.active{display:flex}
52
+
53
+ /* ── Shared card ── */
54
+ .card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px;margin-bottom:12px}
55
+ .card-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-dim);margin-bottom:10px}
56
+
57
+ /* ── Scrollable inner ── */
58
+ .scroll{overflow-y:auto;flex:1;padding:16px 20px}
59
+ .scroll::-webkit-scrollbar{width:6px}
60
+ .scroll::-webkit-scrollbar-track{background:transparent}
61
+ .scroll::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
62
+
63
+ /* ─────────────────── CHAT ─────────────────── */
64
+ #chat-wrap{display:flex;flex:1;flex-direction:column;overflow:hidden}
65
+ #chat-messages{flex:1;overflow-y:auto;padding:16px 20px;display:flex;flex-direction:column;gap:10px}
66
+ #chat-messages::-webkit-scrollbar{width:6px}
67
+ #chat-messages::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
68
+ .msg{max-width:80%;border-radius:10px;padding:10px 14px;line-height:1.55;font-size:13.5px}
69
+ .msg.user{align-self:flex-end;background:var(--orange);color:#fff;border-bottom-right-radius:2px}
70
+ .msg.aiden{align-self:flex-start;background:var(--surface);border:1px solid var(--border);color:var(--text);border-bottom-left-radius:2px}
71
+ .msg.aiden pre{background:var(--bg);border:1px solid var(--border);border-radius:5px;padding:8px 10px;overflow-x:auto;font-size:12px;margin-top:8px}
72
+ .msg.aiden code{font-size:12px;background:var(--bg);padding:1px 4px;border-radius:3px}
73
+ .msg.system{align-self:center;color:var(--text-dim);font-size:12px;font-style:italic}
74
+ .msg.thinking{align-self:flex-start;color:var(--text-dim);font-size:12px;font-style:italic;padding:6px 10px}
75
+ #chat-input-bar{padding:12px 20px;border-top:1px solid var(--border);display:flex;gap:8px;flex-shrink:0;background:var(--surface)}
76
+ #chat-input{flex:1;background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:9px 14px;color:var(--text);font-size:14px;resize:none;min-height:40px;max-height:120px;line-height:1.4;font-family:var(--font)}
77
+ #chat-input:focus{outline:none;border-color:var(--orange)}
78
+ #send-btn{background:var(--orange);color:#fff;border:none;border-radius:8px;padding:9px 18px;font-size:14px;font-weight:600;cursor:pointer;flex-shrink:0;transition:background .15s}
79
+ #send-btn:hover{background:var(--orange-dim)}
80
+ #send-btn:disabled{opacity:.5;cursor:not-allowed}
81
+ #session-bar{padding:6px 20px;font-size:11px;color:var(--text-dim);border-bottom:1px solid var(--border);display:flex;gap:16px;align-items:center;flex-shrink:0;background:var(--surface)}
82
+ .sess-btn{background:transparent;border:1px solid var(--border);color:var(--text-dim);border-radius:5px;padding:2px 8px;font-size:11px;cursor:pointer}
83
+ .sess-btn:hover{border-color:var(--orange);color:var(--orange)}
84
+
85
+ /* ─────────────────── PROVIDERS ─────────────────── */
86
+ .provider-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));gap:12px}
87
+ .pcard{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:14px}
88
+ .pcard-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
89
+ .pcard-name{font-weight:600;font-size:13px}
90
+ .badge{font-size:10px;padding:2px 7px;border-radius:20px;font-weight:600;text-transform:uppercase;letter-spacing:.4px}
91
+ .badge.ok{background:#1a3a1a;color:var(--green)}
92
+ .badge.err{background:#3a1a1a;color:var(--red)}
93
+ .badge.warn{background:#3a2a00;color:var(--yellow)}
94
+ .pcard-model{font-size:11px;color:var(--text-dim);margin-top:2px}
95
+ .pcard-latency{font-size:11px;color:var(--text-dim);margin-top:4px}
96
+
97
+ /* ─────────────────── MEMORY ─────────────────── */
98
+ .mem-item{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:10px 14px;margin-bottom:8px;display:flex;gap:10px;align-items:flex-start}
99
+ .mem-icon{color:var(--orange);font-size:16px;flex-shrink:0;margin-top:1px}
100
+ .mem-text{font-size:13px;line-height:1.5;color:var(--text);flex:1}
101
+ .mem-meta{font-size:10px;color:var(--text-dim);margin-top:3px}
102
+ #mem-search{width:100%;background:var(--surface2);border:1px solid var(--border);border-radius:7px;padding:8px 13px;color:var(--text);font-size:13px;margin-bottom:14px}
103
+ #mem-search:focus{outline:none;border-color:var(--orange)}
104
+ .mem-section-title{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.8px;color:var(--text-dim);margin:14px 0 8px}
105
+
106
+ /* ─────────────────── SKILLS ─────────────────── */
107
+ .skill-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px}
108
+ .scard{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:12px 14px}
109
+ .scard-name{font-weight:600;font-size:13px;color:var(--orange);margin-bottom:4px}
110
+ .scard-desc{font-size:12px;color:var(--text-dim);line-height:1.4}
111
+ .scard-trigger{font-size:10px;color:var(--text-dim);margin-top:5px;font-family:monospace}
112
+
113
+ /* ─────────────────── REFRESH ─────────────────── */
114
+ .refresh-btn{background:transparent;border:1px solid var(--border);color:var(--text-dim);border-radius:6px;padding:5px 12px;font-size:12px;cursor:pointer;transition:all .15s}
115
+ .refresh-btn:hover{border-color:var(--orange);color:var(--orange)}
116
+ .panel-toolbar{display:flex;justify-content:space-between;align-items:center;padding:12px 20px 0;flex-shrink:0}
117
+ .panel-toolbar h2{font-size:14px;font-weight:600;color:var(--text)}
118
+ </style>
119
+ </head>
120
+ <body>
121
+
122
+ <!-- Top bar -->
123
+ <div id="topbar">
124
+ <span id="logo">⬡ Aiden</span>
125
+ <span id="status-dot"></span>
126
+ <span id="status-text">connecting…</span>
127
+ <div id="topbar-right">
128
+ <span id="version-label"></span>
129
+ </div>
130
+ </div>
131
+
132
+ <!-- Tab bar -->
133
+ <div id="tabbar">
134
+ <div class="tab active" data-tab="chat">💬 Chat</div>
135
+ <div class="tab" data-tab="providers">⚡ Providers</div>
136
+ <div class="tab" data-tab="memory">🧠 Memory</div>
137
+ <div class="tab" data-tab="skills">🎯 Skills</div>
138
+ </div>
139
+
140
+ <!-- Panels -->
141
+ <div id="panels">
142
+
143
+ <!-- ── CHAT ── -->
144
+ <div class="panel active" id="panel-chat">
145
+ <div id="session-bar">
146
+ <span id="session-label">Session: —</span>
147
+ <button class="sess-btn" id="new-session-btn">+ New session</button>
148
+ <button class="sess-btn" id="clear-chat-btn">✕ Clear</button>
149
+ </div>
150
+ <div id="chat-wrap">
151
+ <div id="chat-messages">
152
+ <div class="msg system">Aiden is running locally. Ask anything.</div>
153
+ </div>
154
+ <div id="chat-input-bar">
155
+ <textarea id="chat-input" placeholder="Message Aiden…" rows="1"></textarea>
156
+ <button id="send-btn">Send</button>
157
+ </div>
158
+ </div>
159
+ </div>
160
+
161
+ <!-- ── PROVIDERS ── -->
162
+ <div class="panel" id="panel-providers">
163
+ <div class="panel-toolbar">
164
+ <h2>LLM Providers</h2>
165
+ <button class="refresh-btn" id="refresh-providers">↻ Refresh</button>
166
+ </div>
167
+ <div class="scroll">
168
+ <div id="providers-content"><p style="color:var(--text-dim);padding:20px 0">Loading…</p></div>
169
+ </div>
170
+ </div>
171
+
172
+ <!-- ── MEMORY ── -->
173
+ <div class="panel" id="panel-memory">
174
+ <div class="panel-toolbar">
175
+ <h2>Memory</h2>
176
+ <button class="refresh-btn" id="refresh-memory">↻ Refresh</button>
177
+ </div>
178
+ <div class="scroll">
179
+ <input id="mem-search" type="text" placeholder="Search memory…" autocomplete="off"/>
180
+ <div id="memory-content"><p style="color:var(--text-dim)">Loading…</p></div>
181
+ </div>
182
+ </div>
183
+
184
+ <!-- ── SKILLS ── -->
185
+ <div class="panel" id="panel-skills">
186
+ <div class="panel-toolbar">
187
+ <h2>Skills</h2>
188
+ <button class="refresh-btn" id="refresh-skills">↻ Refresh</button>
189
+ </div>
190
+ <div class="scroll">
191
+ <div id="skills-content"><p style="color:var(--text-dim);padding:20px 0">Loading…</p></div>
192
+ </div>
193
+ </div>
194
+
195
+ </div><!-- /panels -->
196
+
197
+ <script>
198
+ // ── Config ──────────────────────────────────────────────────
199
+ const BASE = window.location.origin // e.g. http://localhost:4200
200
+
201
+ // ── Tabs ────────────────────────────────────────────────────
202
+ const tabs = document.querySelectorAll('.tab')
203
+ const panels = document.querySelectorAll('.panel')
204
+ tabs.forEach(tab => {
205
+ tab.addEventListener('click', () => {
206
+ const id = tab.dataset.tab
207
+ tabs.forEach(t => t.classList.remove('active'))
208
+ panels.forEach(p => p.classList.remove('active'))
209
+ tab.classList.add('active')
210
+ document.getElementById('panel-' + id).classList.add('active')
211
+ if (id === 'providers') loadProviders()
212
+ if (id === 'memory') loadMemory()
213
+ if (id === 'skills') loadSkills()
214
+ })
215
+ })
216
+
217
+ // ── Status bar ──────────────────────────────────────────────
218
+ async function ping() {
219
+ try {
220
+ const r = await fetch(BASE + '/api/ping', { cache: 'no-store' })
221
+ const d = await r.json()
222
+ document.getElementById('status-dot').className = 'ok'
223
+ document.getElementById('status-text').textContent = 'online'
224
+ document.getElementById('version-label').textContent = 'v' + (d.version || '')
225
+ } catch {
226
+ document.getElementById('status-dot').className = ''
227
+ document.getElementById('status-text').textContent = 'offline'
228
+ }
229
+ }
230
+ ping()
231
+ setInterval(ping, 10000)
232
+
233
+ // ── Chat ─────────────────────────────────────────────────────
234
+ let sessionId = null
235
+ const chatBox = document.getElementById('chat-messages')
236
+ const chatInput = document.getElementById('chat-input')
237
+ const sendBtn = document.getElementById('send-btn')
238
+
239
+ function addMsg(role, html) {
240
+ const el = document.createElement('div')
241
+ el.className = 'msg ' + role
242
+ el.innerHTML = html
243
+ chatBox.appendChild(el)
244
+ chatBox.scrollTop = chatBox.scrollHeight
245
+ return el
246
+ }
247
+
248
+ function mdToHtml(text) {
249
+ try { return marked.parse(text) } catch { return escHtml(text) }
250
+ }
251
+
252
+ function escHtml(s) {
253
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
254
+ }
255
+
256
+ chatInput.addEventListener('keydown', e => {
257
+ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage() }
258
+ })
259
+
260
+ document.getElementById('send-btn').addEventListener('click', sendMessage)
261
+
262
+ document.getElementById('clear-chat-btn').addEventListener('click', () => {
263
+ chatBox.innerHTML = '<div class="msg system">Cleared.</div>'
264
+ })
265
+
266
+ document.getElementById('new-session-btn').addEventListener('click', () => {
267
+ sessionId = null
268
+ document.getElementById('session-label').textContent = 'Session: —'
269
+ chatBox.innerHTML = '<div class="msg system">New session started.</div>'
270
+ })
271
+
272
+ async function sendMessage() {
273
+ const text = chatInput.value.trim()
274
+ if (!text) return
275
+ chatInput.value = ''
276
+ chatInput.style.height = ''
277
+ sendBtn.disabled = true
278
+ addMsg('user', escHtml(text))
279
+
280
+ const thinkEl = addMsg('thinking', '⋯ thinking')
281
+ let aiEl = null
282
+ let buffer = ''
283
+
284
+ try {
285
+ const body = { message: text }
286
+ if (sessionId) body.sessionId = sessionId
287
+
288
+ const resp = await fetch(BASE + '/api/chat', {
289
+ method: 'POST',
290
+ headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
291
+ body: JSON.stringify(body)
292
+ })
293
+
294
+ const reader = resp.body.getReader()
295
+ const dec = new TextDecoder()
296
+ let partial = ''
297
+
298
+ while (true) {
299
+ const { done, value } = await reader.read()
300
+ if (done) break
301
+ partial += dec.decode(value, { stream: true })
302
+ const lines = partial.split('\\n')
303
+ partial = lines.pop()
304
+ for (const line of lines) {
305
+ if (!line.startsWith('data:')) continue
306
+ const raw = line.slice(5).trim()
307
+ if (!raw || raw === '[DONE]') continue
308
+ let ev
309
+ try { ev = JSON.parse(raw) } catch { continue }
310
+
311
+ if (ev.sessionId && !sessionId) {
312
+ sessionId = ev.sessionId
313
+ document.getElementById('session-label').textContent = 'Session: ' + sessionId.slice(0,8)
314
+ }
315
+ // server sends { token, done: false } — no type field
316
+ if (ev.token || ev.delta) {
317
+ if (thinkEl && thinkEl.parentNode) thinkEl.remove()
318
+ if (!aiEl) aiEl = addMsg('aiden', '')
319
+ buffer += (ev.delta || ev.token || '')
320
+ aiEl.innerHTML = mdToHtml(buffer)
321
+ chatBox.scrollTop = chatBox.scrollHeight
322
+ }
323
+ // server sends { tool: "name", message: "...", timestamp } for tool progress
324
+ if (ev.tool) {
325
+ if (thinkEl && thinkEl.parentNode) thinkEl.innerHTML = '🔧 ' + escHtml(ev.tool)
326
+ }
327
+ // server sends { activity: { icon, message, style, rawTool? }, done: false }
328
+ if (ev.activity) {
329
+ const act = ev.activity
330
+ if (thinkEl && thinkEl.parentNode) {
331
+ thinkEl.innerHTML = (act.icon ? act.icon + ' ' : '🔧 ') + escHtml(act.message || act.rawTool || 'working…')
332
+ }
333
+ if (act.style === 'error' && !buffer) {
334
+ if (thinkEl && thinkEl.parentNode) thinkEl.remove()
335
+ addMsg('system', '⚠ ' + escHtml(act.message || 'Error'))
336
+ }
337
+ }
338
+ // server sends { thinking: { stage, message } }
339
+ if (ev.thinking) {
340
+ if (thinkEl && thinkEl.parentNode) thinkEl.innerHTML = '⋯ ' + escHtml(ev.thinking.message || 'thinking…')
341
+ }
342
+ // server sends { done: true } — no type field
343
+ if (ev.done === true) {
344
+ if (thinkEl && thinkEl.parentNode) thinkEl.remove()
345
+ }
346
+ }
347
+ }
348
+ if (thinkEl && thinkEl.parentNode) thinkEl.remove()
349
+ if (!aiEl && !buffer) addMsg('system', '(no response)')
350
+ } catch (err) {
351
+ if (thinkEl && thinkEl.parentNode) thinkEl.remove()
352
+ addMsg('system', '⚠ ' + escHtml(err.message))
353
+ } finally {
354
+ sendBtn.disabled = false
355
+ chatInput.focus()
356
+ }
357
+ }
358
+
359
+ // Auto-resize textarea
360
+ chatInput.addEventListener('input', () => {
361
+ chatInput.style.height = 'auto'
362
+ chatInput.style.height = Math.min(chatInput.scrollHeight, 120) + 'px'
363
+ })
364
+
365
+ // ── Providers ────────────────────────────────────────────────
366
+ async function loadProviders() {
367
+ const el = document.getElementById('providers-content')
368
+ try {
369
+ const [stateRes, statusRes] = await Promise.all([
370
+ fetch(BASE + '/api/providers/state').then(r => r.json()),
371
+ fetch(BASE + '/api/providers/status').then(r => r.json()).catch(() => ({}))
372
+ ])
373
+
374
+ const providers = Array.isArray(stateRes) ? stateRes
375
+ : (stateRes.providers || Object.entries(stateRes).map(([k,v]) => ({ name: k, ...v })))
376
+
377
+ if (!providers.length) { el.innerHTML = '<p style="color:var(--text-dim)">No providers configured.</p>'; return }
378
+
379
+ const statusMap = {}
380
+ if (statusRes && typeof statusRes === 'object') {
381
+ const arr = Array.isArray(statusRes) ? statusRes : Object.entries(statusRes).map(([k,v])=>({name:k,...v}))
382
+ arr.forEach(p => { statusMap[p.name || p.id] = p })
383
+ }
384
+
385
+ el.innerHTML = '<div class="provider-grid">' + providers.map(p => {
386
+ const name = p.name || p.id || '?'
387
+ const status = p.status || statusMap[name]?.status || 'unknown'
388
+ const badgeCls = status === 'ok' || status === 'healthy' || status === 'active' ? 'ok'
389
+ : status === 'error' || status === 'down' ? 'err' : 'warn'
390
+ const model = p.currentModel || p.model || p.defaultModel || ''
391
+ const latency = p.latency || p.avgLatency || statusMap[name]?.latency || ''
392
+ return \`<div class="pcard">
393
+ <div class="pcard-header">
394
+ <span class="pcard-name">\${escHtml(name)}</span>
395
+ <span class="badge \${badgeCls}">\${escHtml(status)}</span>
396
+ </div>
397
+ \${model ? \`<div class="pcard-model">Model: \${escHtml(model)}</div>\` : ''}
398
+ \${latency ? \`<div class="pcard-latency">Latency: \${typeof latency==='number' ? latency+'ms' : escHtml(String(latency))}</div>\` : ''}
399
+ </div>\`
400
+ }).join('') + '</div>'
401
+ } catch (err) {
402
+ el.innerHTML = '<p style="color:var(--red)">Failed to load providers: ' + escHtml(err.message) + '</p>'
403
+ }
404
+ }
405
+ document.getElementById('refresh-providers').addEventListener('click', loadProviders)
406
+
407
+ // ── Memory ───────────────────────────────────────────────────
408
+ let allMemories = []
409
+
410
+ async function loadMemory() {
411
+ const el = document.getElementById('memory-content')
412
+ try {
413
+ const data = await fetch(BASE + '/api/memory').then(r => r.json())
414
+ allMemories = Array.isArray(data) ? data : (data.memories || data.items || [])
415
+ renderMemory(allMemories)
416
+ } catch (err) {
417
+ el.innerHTML = '<p style="color:var(--red)">Failed to load memory: ' + escHtml(err.message) + '</p>'
418
+ }
419
+ }
420
+
421
+ function renderMemory(items) {
422
+ const el = document.getElementById('memory-content')
423
+ if (!items.length) { el.innerHTML = '<p style="color:var(--text-dim)">No memories stored yet.</p>'; return }
424
+ el.innerHTML = items.map(m => {
425
+ const text = m.content || m.text || m.value || JSON.stringify(m)
426
+ const meta = [m.type, m.category, m.created_at || m.createdAt].filter(Boolean).join(' · ')
427
+ return \`<div class="mem-item">
428
+ <span class="mem-icon">◈</span>
429
+ <div>
430
+ <div class="mem-text">\${escHtml(String(text))}</div>
431
+ \${meta ? \`<div class="mem-meta">\${escHtml(meta)}</div>\` : ''}
432
+ </div>
433
+ </div>\`
434
+ }).join('')
435
+ }
436
+
437
+ let memSearchTimer = null
438
+ document.getElementById('mem-search').addEventListener('input', e => {
439
+ clearTimeout(memSearchTimer)
440
+ const q = e.target.value.trim()
441
+ memSearchTimer = setTimeout(async () => {
442
+ if (!q) { renderMemory(allMemories); return }
443
+ try {
444
+ const data = await fetch(BASE + '/api/memory/search?q=' + encodeURIComponent(q)).then(r => r.json())
445
+ const results = Array.isArray(data) ? data : (data.results || data.memories || [])
446
+ renderMemory(results)
447
+ } catch { renderMemory(allMemories.filter(m => JSON.stringify(m).toLowerCase().includes(q.toLowerCase()))) }
448
+ }, 300)
449
+ })
450
+ document.getElementById('refresh-memory').addEventListener('click', loadMemory)
451
+
452
+ // ── Skills ───────────────────────────────────────────────────
453
+ async function loadSkills() {
454
+ const el = document.getElementById('skills-content')
455
+ try {
456
+ const data = await fetch(BASE + '/api/skills/learned').then(r => r.json())
457
+ const skills = Array.isArray(data) ? data : (data.skills || data.items || [])
458
+ if (!skills.length) { el.innerHTML = '<p style="color:var(--text-dim)">No skills loaded.</p>'; return }
459
+ el.innerHTML = '<div class="skill-grid">' + skills.map(s => {
460
+ const name = s.name || s.id || '?'
461
+ const desc = s.description || s.desc || ''
462
+ const trigger = s.trigger || s.command || ''
463
+ return \`<div class="scard">
464
+ <div class="scard-name">\${escHtml(name)}</div>
465
+ \${desc ? \`<div class="scard-desc">\${escHtml(desc)}</div>\` : ''}
466
+ \${trigger ? \`<div class="scard-trigger">trigger: \${escHtml(trigger)}</div>\` : ''}
467
+ </div>\`
468
+ }).join('') + '</div>'
469
+ } catch (err) {
470
+ el.innerHTML = '<p style="color:var(--red)">Failed to load skills: ' + escHtml(err.message) + '</p>'
471
+ }
472
+ }
473
+ document.getElementById('refresh-skills').addEventListener('click', loadSkills)
474
+
475
+ // ── Init ─────────────────────────────────────────────────────
476
+ chatInput.focus()
477
+ </script>
478
+ </body>
479
+ </html>`;
480
+ }