@yemi33/minions 0.1.2195 → 0.1.2196

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,576 @@
1
+
2
+ // ── Knowledge control panel (Pinned Context + Notes + KB) ──────────
3
+ // One unified cockpit box over the three knowledge surfaces, replacing the
4
+ // old single "Pinned context" tile. Reuses the classic dashboard's existing
5
+ // endpoints (no new server routes):
6
+ // Pinned — /api/pinned (+ update/remove); the Pinned-Context tab reuses the
7
+ // editor + list renderer in pinned.js.
8
+ // Notes — /api/notes-full (read notes.md), /api/notes-save (edit notes.md),
9
+ // /api/notes (add an inbox note for consolidation).
10
+ // KB — /api/knowledge (list), /api/knowledge/:cat/:file (read entry),
11
+ // /api/knowledge POST (create), /api/kb-pins(+/toggle) (pin state).
12
+ //
13
+ // Counts: pinned is live from the 5s status poll (renderKnowledgeTile is fed
14
+ // by applyStatus). Notes + KB counts are fetched lazily — once shortly after
15
+ // load and again after any create/save — so the heavier /api/notes-full and
16
+ // /api/knowledge reads stay off the recurring poll.
17
+
18
+ var _knowledgeCounts = { pinned: 0, notes: null, kb: null };
19
+ var _knActiveTab = 'pinned';
20
+ var _kbData = null; // cached /api/knowledge payload
21
+ var _kbPins = []; // cached pinned KB keys ('knowledge/<cat>/<file>')
22
+
23
+ var KB_CAT_LABELS = {
24
+ architecture: 'Architecture', conventions: 'Conventions',
25
+ 'project-notes': 'Project Notes', 'build-reports': 'Build Reports',
26
+ reviews: 'Reviews', learnings: 'Learnings', decisions: 'Decisions',
27
+ incidents: 'Incidents', 'api-notes': 'API Notes',
28
+ };
29
+ // Categories offered in the KB create form (mirrors the classic dashboard).
30
+ var KB_CREATE_CATS = ['architecture', 'conventions', 'project-notes', 'build-reports', 'reviews'];
31
+ var KB_LIST_CAP = 60; // cap rendered rows; the KB can hold thousands
32
+
33
+ // ── Cockpit tile ───────────────────────────────────────────────────
34
+ // Called from applyStatus with the live pinned count each poll, and from the
35
+ // lazy count loader (no arg) to repaint when notes/KB counts arrive.
36
+ function renderKnowledgeTile(pinnedCount) {
37
+ if (typeof pinnedCount === 'number') _knowledgeCounts.pinned = pinnedCount;
38
+ var p = _knowledgeCounts.pinned || 0;
39
+ var n = _knowledgeCounts.notes;
40
+ var k = _knowledgeCounts.kb;
41
+ var total = p + (n || 0) + (k || 0);
42
+ var detail = p + ' pinned · ' + (n == null ? '…' : n) + ' notes · ' + (k == null ? '…' : k) + ' KB';
43
+ updateTile('knowledge', total, detail, total ? 'blue' : null);
44
+ }
45
+
46
+ function _countNotesEntries(text) {
47
+ if (!text || text === 'No notes file found.') return 0;
48
+ var m = text.match(/^###\s/gm);
49
+ return m ? m.length : 0;
50
+ }
51
+
52
+ function _countKbEntries(data) {
53
+ if (!data || typeof data !== 'object') return 0;
54
+ var total = 0;
55
+ Object.keys(data).forEach(function(k) {
56
+ if (Array.isArray(data[k])) total += data[k].length;
57
+ });
58
+ return total;
59
+ }
60
+
61
+ // Best-effort lazy fetch of the notes + KB counts. Failures leave the cached
62
+ // value untouched so a transient error doesn't blank the tile detail.
63
+ async function loadKnowledgeCounts() {
64
+ try {
65
+ var res = await fetch('/api/notes-full', { headers: { 'Accept': 'text/plain' } });
66
+ if (res.ok) _knowledgeCounts.notes = _countNotesEntries(await res.text());
67
+ } catch (e) { /* keep last-known */ }
68
+ try {
69
+ var kres = await fetch('/api/knowledge', { headers: { 'Accept': 'application/json' } });
70
+ if (kres.ok) {
71
+ _kbData = await kres.json();
72
+ _knowledgeCounts.kb = _countKbEntries(_kbData);
73
+ }
74
+ } catch (e) { /* keep last-known */ }
75
+ renderKnowledgeTile();
76
+ }
77
+
78
+ async function loadKbPins() {
79
+ try {
80
+ var res = await fetch('/api/kb-pins', { headers: { 'Accept': 'application/json' } });
81
+ if (!res.ok) return;
82
+ var d = await res.json();
83
+ _kbPins = Array.isArray(d.pins) ? d.pins : [];
84
+ } catch (e) { _kbPins = _kbPins || []; }
85
+ }
86
+
87
+ function kbPinKey(cat, file) { return 'knowledge/' + cat + '/' + file; }
88
+ function isKbPinned(key) { return _kbPins.indexOf(key) !== -1; }
89
+
90
+ // ── Modal open/close + tab switching ───────────────────────────────
91
+ function openKnowledgeModal() {
92
+ var modal = document.getElementById('slim-knowledge-modal');
93
+ if (!modal) return;
94
+ renderKnowledgeTab();
95
+ modal.classList.add('open');
96
+ }
97
+ function closeKnowledgeModal() {
98
+ var modal = document.getElementById('slim-knowledge-modal');
99
+ if (modal) modal.classList.remove('open');
100
+ }
101
+ function setKnowledgeTab(tab) {
102
+ _knActiveTab = tab;
103
+ var tabs = document.querySelectorAll('#slim-kn-tabs .kn-tab');
104
+ var activeTabId = null;
105
+ for (var i = 0; i < tabs.length; i++) {
106
+ var isActive = tabs[i].getAttribute('data-kn-tab') === tab;
107
+ tabs[i].classList.toggle('active', isActive);
108
+ tabs[i].setAttribute('aria-selected', isActive ? 'true' : 'false');
109
+ if (isActive) activeTabId = tabs[i].id;
110
+ }
111
+ var body = document.getElementById('slim-knowledge-body');
112
+ if (body && activeTabId) body.setAttribute('aria-labelledby', activeTabId);
113
+ renderKnowledgeTab();
114
+ }
115
+ function renderKnowledgeTab() {
116
+ var body = document.getElementById('slim-knowledge-body');
117
+ if (!body) return;
118
+ body.textContent = '';
119
+ if (_knActiveTab === 'notes') renderKnowledgeNotesTab(body);
120
+ else if (_knActiveTab === 'kb') renderKnowledgeKbTab(body);
121
+ else renderKnowledgePinnedTab(body);
122
+ }
123
+
124
+ // ── Tab: Pinned Context ────────────────────────────────────────────
125
+ function renderKnowledgePinnedTab(body) {
126
+ var intro = document.createElement('p');
127
+ intro.textContent = 'Context all agents see, prepended to every prompt as “read first”.';
128
+ body.appendChild(intro);
129
+
130
+ var toolbar = document.createElement('div');
131
+ toolbar.className = 'kn-toolbar';
132
+ var addBtn = document.createElement('button');
133
+ addBtn.className = 'linkpr-chip';
134
+ addBtn.type = 'button';
135
+ addBtn.textContent = '+ Pin content';
136
+ addBtn.addEventListener('click', function() { openSlimPinEditor(null); });
137
+ toolbar.appendChild(addBtn);
138
+ body.appendChild(toolbar);
139
+
140
+ var list = document.createElement('div');
141
+ list.id = 'slim-pinned-list';
142
+ body.appendChild(list);
143
+ renderSlimPinnedList(); // defined in pinned.js; renders into #slim-pinned-list
144
+ }
145
+
146
+ // ── Tab: Notes ─────────────────────────────────────────────────────
147
+ function renderKnowledgeNotesTab(body) {
148
+ var intro = document.createElement('p');
149
+ intro.textContent = 'Consolidated team notes (notes.md). Edit and save, or add a note for the next consolidation sweep.';
150
+ body.appendChild(intro);
151
+
152
+ var msg = document.createElement('div');
153
+ msg.id = 'slim-notes-msg';
154
+ msg.className = 'kn-msg';
155
+ body.appendChild(msg);
156
+
157
+ var ta = document.createElement('textarea');
158
+ ta.id = 'slim-notes-content';
159
+ ta.className = 'linkpr-input kn-notes-textarea';
160
+ ta.rows = 12;
161
+ ta.value = 'Loading notes…';
162
+ ta.disabled = true;
163
+ body.appendChild(ta);
164
+
165
+ var saveRow = document.createElement('div');
166
+ saveRow.className = 'kn-toolbar kn-toolbar-end';
167
+ var saveBtn = document.createElement('button');
168
+ saveBtn.className = 'btn-primary';
169
+ saveBtn.type = 'button';
170
+ saveBtn.textContent = 'Save notes.md';
171
+ saveBtn.disabled = true;
172
+ saveBtn.addEventListener('click', saveKnowledgeNotes);
173
+ saveRow.appendChild(saveBtn);
174
+ body.appendChild(saveRow);
175
+
176
+ // Add-a-note (inbox) sub-form.
177
+ var addHead = document.createElement('div');
178
+ addHead.className = 'kn-section-head';
179
+ addHead.textContent = 'Add a note for consolidation';
180
+ body.appendChild(addHead);
181
+
182
+ var titleLabel = document.createElement('label');
183
+ titleLabel.className = 'linkpr-label';
184
+ titleLabel.textContent = 'Title';
185
+ var titleInput = document.createElement('input');
186
+ titleInput.id = 'slim-note-title';
187
+ titleInput.className = 'linkpr-input';
188
+ titleInput.type = 'text';
189
+ titleInput.placeholder = 'Short summary';
190
+ titleLabel.appendChild(titleInput);
191
+ body.appendChild(titleLabel);
192
+
193
+ var whatLabel = document.createElement('label');
194
+ whatLabel.className = 'linkpr-label';
195
+ whatLabel.textContent = 'Note';
196
+ var whatInput = document.createElement('textarea');
197
+ whatInput.id = 'slim-note-what';
198
+ whatInput.className = 'linkpr-input';
199
+ whatInput.rows = 3;
200
+ whatInput.placeholder = 'What did you learn / decide?';
201
+ whatLabel.appendChild(whatInput);
202
+ body.appendChild(whatLabel);
203
+
204
+ var addRow = document.createElement('div');
205
+ addRow.className = 'kn-toolbar kn-toolbar-end';
206
+ var addNoteMsg = document.createElement('span');
207
+ addNoteMsg.id = 'slim-note-msg';
208
+ addNoteMsg.className = 'kn-msg kn-msg-inline';
209
+ addRow.appendChild(addNoteMsg);
210
+ var addNoteBtn = document.createElement('button');
211
+ addNoteBtn.className = 'btn-secondary';
212
+ addNoteBtn.type = 'button';
213
+ addNoteBtn.textContent = 'Add note';
214
+ addNoteBtn.addEventListener('click', submitKnowledgeNote);
215
+ addRow.appendChild(addNoteBtn);
216
+ body.appendChild(addRow);
217
+
218
+ // Load notes.md into the textarea.
219
+ fetch('/api/notes-full', { headers: { 'Accept': 'text/plain' } })
220
+ .then(function(r) { return r.ok ? r.text() : Promise.reject(new Error('HTTP ' + r.status)); })
221
+ .then(function(text) {
222
+ if (document.getElementById('slim-notes-content') !== ta) return; // tab switched away
223
+ ta.value = (text === 'No notes file found.') ? '' : text;
224
+ ta.disabled = false;
225
+ saveBtn.disabled = false;
226
+ _knowledgeCounts.notes = _countNotesEntries(text);
227
+ renderKnowledgeTile();
228
+ })
229
+ .catch(function(e) {
230
+ ta.value = '';
231
+ ta.disabled = false;
232
+ saveBtn.disabled = false;
233
+ if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Could not load notes: ' + (e.message || e); }
234
+ });
235
+ }
236
+
237
+ async function saveKnowledgeNotes() {
238
+ var ta = document.getElementById('slim-notes-content');
239
+ var msg = document.getElementById('slim-notes-msg');
240
+ if (!ta) return;
241
+ if (msg) { msg.style.color = 'var(--muted)'; msg.textContent = 'Saving…'; }
242
+ try {
243
+ var res = await fetch('/api/notes-save', {
244
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
245
+ body: JSON.stringify({ content: ta.value, file: 'notes.md' }),
246
+ });
247
+ var d = await res.json().catch(function() { return {}; });
248
+ if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
249
+ if (msg) { msg.style.color = 'var(--green)'; msg.textContent = 'Saved.'; }
250
+ _knowledgeCounts.notes = _countNotesEntries(ta.value);
251
+ renderKnowledgeTile();
252
+ } catch (e) {
253
+ if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Error: ' + (e && e.message ? e.message : 'failed'); }
254
+ }
255
+ }
256
+
257
+ async function submitKnowledgeNote() {
258
+ var titleEl = document.getElementById('slim-note-title');
259
+ var whatEl = document.getElementById('slim-note-what');
260
+ var msg = document.getElementById('slim-note-msg');
261
+ var title = titleEl ? (titleEl.value || '').trim() : '';
262
+ var what = whatEl ? (whatEl.value || '').trim() : '';
263
+ if (!title) {
264
+ if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Title is required.'; }
265
+ return;
266
+ }
267
+ if (msg) { msg.style.color = 'var(--muted)'; msg.textContent = 'Adding…'; }
268
+ try {
269
+ var res = await fetch('/api/notes', {
270
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
271
+ body: JSON.stringify({ title: title, what: what }),
272
+ });
273
+ var d = await res.json().catch(function() { return {}; });
274
+ if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
275
+ if (titleEl) titleEl.value = '';
276
+ if (whatEl) whatEl.value = '';
277
+ if (msg) { msg.style.color = 'var(--green)'; msg.textContent = 'Note added for consolidation.'; }
278
+ } catch (e) {
279
+ if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Error: ' + (e && e.message ? e.message : 'failed'); }
280
+ }
281
+ }
282
+
283
+ // ── Tab: KB ────────────────────────────────────────────────────────
284
+ function renderKnowledgeKbTab(body) {
285
+ var intro = document.createElement('p');
286
+ intro.textContent = 'Knowledge base — categorized entries written by consolidation. Click an entry to read it.';
287
+ body.appendChild(intro);
288
+
289
+ var toolbar = document.createElement('div');
290
+ toolbar.className = 'kn-toolbar';
291
+ var newBtn = document.createElement('button');
292
+ newBtn.className = 'linkpr-chip';
293
+ newBtn.type = 'button';
294
+ newBtn.textContent = '+ New entry';
295
+ newBtn.addEventListener('click', function() { toggleKbCreateForm(body); });
296
+ toolbar.appendChild(newBtn);
297
+ body.appendChild(toolbar);
298
+
299
+ var listMsg = document.createElement('div');
300
+ listMsg.id = 'slim-kb-list-msg';
301
+ listMsg.className = 'kn-msg';
302
+ body.appendChild(listMsg);
303
+
304
+ var listWrap = document.createElement('div');
305
+ listWrap.id = 'slim-kb-list';
306
+ body.appendChild(listWrap);
307
+
308
+ if (_kbData) renderKbList(listWrap);
309
+ else {
310
+ var loading = document.createElement('div');
311
+ loading.className = 'tile-empty';
312
+ loading.textContent = 'Loading knowledge base…';
313
+ listWrap.appendChild(loading);
314
+ }
315
+ // Always refresh list + pins on open so newly-swept entries appear.
316
+ Promise.all([
317
+ fetch('/api/knowledge', { headers: { 'Accept': 'application/json' } }).then(function(r) { return r.ok ? r.json() : null; }).catch(function() { return null; }),
318
+ loadKbPins(),
319
+ ]).then(function(results) {
320
+ if (results[0]) {
321
+ _kbData = results[0];
322
+ _knowledgeCounts.kb = _countKbEntries(_kbData);
323
+ renderKnowledgeTile();
324
+ }
325
+ if (_knActiveTab === 'kb') {
326
+ var lw = document.getElementById('slim-kb-list');
327
+ if (lw) renderKbList(lw);
328
+ }
329
+ });
330
+ }
331
+
332
+ function _kbAllItems() {
333
+ var items = [];
334
+ if (!_kbData) return items;
335
+ Object.keys(_kbData).forEach(function(cat) {
336
+ if (!Array.isArray(_kbData[cat])) return;
337
+ _kbData[cat].forEach(function(item) {
338
+ items.push({ file: item.file, category: cat, title: item.title, agent: item.agent, date: item.date, sortTs: item.sortTs, preview: item.preview });
339
+ });
340
+ });
341
+ return items;
342
+ }
343
+
344
+ function renderKbList(listWrap) {
345
+ listWrap.textContent = '';
346
+ var items = _kbAllItems();
347
+ if (!items.length) {
348
+ var empty = document.createElement('div');
349
+ empty.className = 'tile-empty';
350
+ empty.textContent = 'No knowledge entries yet. Notes are classified here after consolidation.';
351
+ listWrap.appendChild(empty);
352
+ return;
353
+ }
354
+ // Pinned first, then newest.
355
+ items.sort(function(a, b) {
356
+ var ap = isKbPinned(kbPinKey(a.category, a.file));
357
+ var bp = isKbPinned(kbPinKey(b.category, b.file));
358
+ if (ap !== bp) return ap ? -1 : 1;
359
+ return (b.sortTs || 0) - (a.sortTs || 0) || (b.date || '').localeCompare(a.date || '');
360
+ });
361
+ var shown = items.slice(0, KB_LIST_CAP);
362
+ shown.forEach(function(item) {
363
+ listWrap.appendChild(buildKbRow(item));
364
+ });
365
+ if (items.length > shown.length) {
366
+ var more = document.createElement('div');
367
+ more.className = 'tile-empty';
368
+ more.textContent = 'Showing ' + shown.length + ' of ' + items.length + ' entries (newest + pinned first).';
369
+ listWrap.appendChild(more);
370
+ }
371
+ }
372
+
373
+ function buildKbRow(item) {
374
+ var key = kbPinKey(item.category, item.file);
375
+ var pinned = isKbPinned(key);
376
+ var row = document.createElement('div');
377
+ row.className = 'kb-row' + (pinned ? ' kb-row-pinned' : '');
378
+
379
+ var top = document.createElement('div');
380
+ top.className = 'kb-row-top';
381
+
382
+ var pinBtn = document.createElement('button');
383
+ pinBtn.className = 'kb-pin-btn' + (pinned ? ' on' : '');
384
+ pinBtn.type = 'button';
385
+ pinBtn.title = pinned ? 'Unpin from KB' : 'Pin in KB';
386
+ pinBtn.textContent = pinned ? '★' : '☆';
387
+ pinBtn.addEventListener('click', function(ev) { ev.stopPropagation(); toggleKbPin(key); });
388
+ top.appendChild(pinBtn);
389
+
390
+ var title = document.createElement('span');
391
+ title.className = 'kb-row-title';
392
+ title.textContent = item.title || item.file || '(untitled)';
393
+ title.title = item.title || '';
394
+ top.appendChild(title);
395
+ row.appendChild(top);
396
+
397
+ var meta = document.createElement('div');
398
+ meta.className = 'kb-row-meta';
399
+ meta.textContent = [KB_CAT_LABELS[item.category] || item.category, item.agent || null, item.date || null]
400
+ .filter(Boolean).join(' · ');
401
+ row.appendChild(meta);
402
+
403
+ if (item.preview) {
404
+ var prev = document.createElement('div');
405
+ prev.className = 'kb-row-preview';
406
+ var t = String(item.preview);
407
+ prev.textContent = t.length > 200 ? t.slice(0, 200) + '…' : t;
408
+ row.appendChild(prev);
409
+ }
410
+
411
+ row.addEventListener('click', function() { openKbItem(item.category, item.file); });
412
+ return row;
413
+ }
414
+
415
+ async function toggleKbPin(key) {
416
+ var listMsg = document.getElementById('slim-kb-list-msg');
417
+ if (listMsg) { listMsg.style.color = ''; listMsg.textContent = ''; }
418
+ try {
419
+ var res = await fetch('/api/kb-pins/toggle', {
420
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
421
+ body: JSON.stringify({ key: key }),
422
+ });
423
+ var d = await res.json().catch(function() { return {}; });
424
+ if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
425
+ if (d.pinned) { if (_kbPins.indexOf(key) === -1) _kbPins.push(key); }
426
+ else { _kbPins = _kbPins.filter(function(k) { return k !== key; }); }
427
+ var lw = document.getElementById('slim-kb-list');
428
+ if (lw) renderKbList(lw);
429
+ } catch (e) {
430
+ if (listMsg) { listMsg.style.color = 'var(--red)'; listMsg.textContent = 'Pin toggle failed: ' + (e && e.message ? e.message : e); }
431
+ }
432
+ }
433
+
434
+ async function openKbItem(category, file) {
435
+ var body = document.getElementById('slim-knowledge-body');
436
+ if (!body) return;
437
+ body.textContent = '';
438
+ var backRow = document.createElement('div');
439
+ backRow.className = 'kn-toolbar';
440
+ var backBtn = document.createElement('button');
441
+ backBtn.className = 'btn-secondary';
442
+ backBtn.type = 'button';
443
+ backBtn.textContent = '← Back to list';
444
+ backBtn.addEventListener('click', function() { renderKnowledgeTab(); });
445
+ backRow.appendChild(backBtn);
446
+ body.appendChild(backRow);
447
+
448
+ var head = document.createElement('div');
449
+ head.className = 'kn-section-head';
450
+ head.textContent = file;
451
+ body.appendChild(head);
452
+
453
+ var pre = document.createElement('pre');
454
+ pre.className = 'kb-entry-content';
455
+ pre.textContent = 'Loading…';
456
+ body.appendChild(pre);
457
+
458
+ try {
459
+ var res = await fetch('/api/knowledge/' + encodeURIComponent(category) + '/' + encodeURIComponent(file));
460
+ if (!res.ok) throw new Error('HTTP ' + res.status);
461
+ var text = await res.text();
462
+ pre.textContent = text.replace(/^---[\s\S]*?---\n*/m, '');
463
+ } catch (e) {
464
+ pre.textContent = 'Failed to load entry: ' + (e && e.message ? e.message : e);
465
+ }
466
+ }
467
+
468
+ function toggleKbCreateForm(body) {
469
+ var existing = document.getElementById('slim-kb-create');
470
+ if (existing) { existing.parentNode.removeChild(existing); return; }
471
+ var form = document.createElement('div');
472
+ form.id = 'slim-kb-create';
473
+ form.className = 'kn-create-form';
474
+
475
+ var catLabel = document.createElement('label');
476
+ catLabel.className = 'linkpr-label';
477
+ catLabel.textContent = 'Category';
478
+ var catSel = document.createElement('select');
479
+ catSel.id = 'slim-kb-category';
480
+ catSel.className = 'linkpr-input';
481
+ KB_CREATE_CATS.forEach(function(c) {
482
+ var opt = document.createElement('option');
483
+ opt.value = c;
484
+ opt.textContent = KB_CAT_LABELS[c] || c;
485
+ catSel.appendChild(opt);
486
+ });
487
+ catLabel.appendChild(catSel);
488
+ form.appendChild(catLabel);
489
+
490
+ var titleLabel = document.createElement('label');
491
+ titleLabel.className = 'linkpr-label';
492
+ titleLabel.textContent = 'Title';
493
+ var titleInput = document.createElement('input');
494
+ titleInput.id = 'slim-kb-title';
495
+ titleInput.className = 'linkpr-input';
496
+ titleInput.type = 'text';
497
+ titleInput.placeholder = 'Entry title';
498
+ titleLabel.appendChild(titleInput);
499
+ form.appendChild(titleLabel);
500
+
501
+ var contentLabel = document.createElement('label');
502
+ contentLabel.className = 'linkpr-label';
503
+ contentLabel.textContent = 'Content';
504
+ var contentInput = document.createElement('textarea');
505
+ contentInput.id = 'slim-kb-content';
506
+ contentInput.className = 'linkpr-input';
507
+ contentInput.rows = 6;
508
+ contentInput.placeholder = 'Write your knowledge entry…';
509
+ contentLabel.appendChild(contentInput);
510
+ form.appendChild(contentLabel);
511
+
512
+ var row = document.createElement('div');
513
+ row.className = 'kn-toolbar kn-toolbar-end';
514
+ var msg = document.createElement('span');
515
+ msg.id = 'slim-kb-create-msg';
516
+ msg.className = 'kn-msg kn-msg-inline';
517
+ row.appendChild(msg);
518
+ var saveBtn = document.createElement('button');
519
+ saveBtn.className = 'btn-primary';
520
+ saveBtn.type = 'button';
521
+ saveBtn.textContent = 'Save entry';
522
+ saveBtn.addEventListener('click', submitKbEntry);
523
+ row.appendChild(saveBtn);
524
+ form.appendChild(row);
525
+
526
+ // Insert the form directly after the toolbar (before the list).
527
+ var listWrap = document.getElementById('slim-kb-list');
528
+ if (listWrap && listWrap.parentNode) listWrap.parentNode.insertBefore(form, listWrap);
529
+ else body.appendChild(form);
530
+ }
531
+
532
+ async function submitKbEntry() {
533
+ var catEl = document.getElementById('slim-kb-category');
534
+ var titleEl = document.getElementById('slim-kb-title');
535
+ var contentEl = document.getElementById('slim-kb-content');
536
+ var msg = document.getElementById('slim-kb-create-msg');
537
+ var category = catEl ? catEl.value : '';
538
+ var title = titleEl ? (titleEl.value || '').trim() : '';
539
+ var content = contentEl ? contentEl.value : '';
540
+ if (!title || !content.trim()) {
541
+ if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Title and content are required.'; }
542
+ return;
543
+ }
544
+ if (msg) { msg.style.color = 'var(--muted)'; msg.textContent = 'Saving…'; }
545
+ try {
546
+ var res = await fetch('/api/knowledge', {
547
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
548
+ body: JSON.stringify({ category: category, title: title, content: content }),
549
+ });
550
+ var d = await res.json().catch(function() { return {}; });
551
+ if (!res.ok) throw new Error(d.error || ('HTTP ' + res.status));
552
+ // Refresh the list + count, then re-render the KB tab.
553
+ var kres = await fetch('/api/knowledge', { headers: { 'Accept': 'application/json' } });
554
+ if (kres.ok) { _kbData = await kres.json(); _knowledgeCounts.kb = _countKbEntries(_kbData); renderKnowledgeTile(); }
555
+ if (_knActiveTab === 'kb') renderKnowledgeTab();
556
+ } catch (e) {
557
+ if (msg) { msg.style.color = 'var(--red)'; msg.textContent = 'Error: ' + (e && e.message ? e.message : 'failed'); }
558
+ }
559
+ }
560
+
561
+ // ── Wiring ─────────────────────────────────────────────────────────
562
+ bindModalClose('slim-knowledge-modal', 'slim-knowledge-close');
563
+ (function bindKnowledgeUi() {
564
+ var tabBar = document.getElementById('slim-kn-tabs');
565
+ if (tabBar) {
566
+ tabBar.addEventListener('click', function(ev) {
567
+ var btn = ev.target && ev.target.closest ? ev.target.closest('.kn-tab') : null;
568
+ if (!btn) return;
569
+ var tab = btn.getAttribute('data-kn-tab');
570
+ if (tab) setKnowledgeTab(tab);
571
+ });
572
+ }
573
+ // Populate the notes + KB counts shortly after first paint so the tile
574
+ // detail reads "N pinned · M notes · K KB" without waiting for a modal open.
575
+ setTimeout(loadKnowledgeCounts, 1500);
576
+ })();
@@ -69,6 +69,29 @@
69
69
  grid.appendChild(frag);
70
70
  }
71
71
 
72
+ // Live "Working for" ticker for the agent detail modal. Mirrors the legacy
73
+ // renderer's .agent-runtime-tick pattern (dashboard/js/render-agents.js): a
74
+ // data-started element refreshed by a 1s interval, cleared on modal close so
75
+ // no interval leaks once the popout is dismissed.
76
+ var _agentDetailTimer = null;
77
+
78
+ // Format an elapsed-ms span as 'Xh Ym Zs', dropping the hours segment when 0.
79
+ function _fmtAgentElapsed(ms) {
80
+ if (!(ms > 0)) ms = 0;
81
+ var sec = Math.floor(ms / 1000) % 60, min = Math.floor(ms / 60000) % 60, hr = Math.floor(ms / 3600000);
82
+ return (hr > 0 ? hr + 'h ' : '') + min + 'm ' + sec + 's';
83
+ }
84
+
85
+ function _tickAgentDetailRuntime() {
86
+ var el = document.getElementById('slim-agent-working-tick');
87
+ if (!el) { _stopAgentDetailRuntime(); return; }
88
+ el.textContent = _fmtAgentElapsed(Date.now() - new Date(el.dataset.started).getTime());
89
+ }
90
+
91
+ function _stopAgentDetailRuntime() {
92
+ if (_agentDetailTimer) { clearInterval(_agentDetailTimer); _agentDetailTimer = null; }
93
+ }
94
+
72
95
  // Append a labelled key/value row to the agent detail modal body.
73
96
  function appendAgentRow(body, key, value, muted) {
74
97
  var row = document.createElement('div');
@@ -92,6 +115,8 @@
92
115
  var titleEl = document.getElementById('slim-agent-title');
93
116
  if (!a || !modal || !body) return;
94
117
  if (titleEl) titleEl.textContent = a.name || a.id;
118
+ // Clear any ticker left over from a previously-opened agent before rebuild.
119
+ _stopAgentDetailRuntime();
95
120
  body.textContent = '';
96
121
 
97
122
  // Header: emoji + name + role.
@@ -131,6 +156,24 @@
131
156
 
132
157
  appendAgentRow(body, 'Last result', a.resultSummary || 'No recent output', !a.resultSummary);
133
158
 
159
+ // Only running agents get a live elapsed-time row; idle/done/error don't.
160
+ if (a.status === 'working' && a.started_at) {
161
+ var workRow = document.createElement('div');
162
+ workRow.className = 'agent-detail-row';
163
+ var workKey = document.createElement('div');
164
+ workKey.className = 'agent-detail-key';
165
+ workKey.textContent = 'Working for';
166
+ var workVal = document.createElement('div');
167
+ workVal.className = 'agent-detail-val';
168
+ workVal.id = 'slim-agent-working-tick';
169
+ workVal.dataset.started = a.started_at;
170
+ workRow.appendChild(workKey);
171
+ workRow.appendChild(workVal);
172
+ body.appendChild(workRow);
173
+ _tickAgentDetailRuntime();
174
+ _agentDetailTimer = setInterval(_tickAgentDetailRuntime, 1000);
175
+ }
176
+
134
177
  modal.classList.add('open');
135
178
  }
136
179
 
@@ -12,7 +12,9 @@
12
12
  if (ev.key === 'Escape' && modal.classList.contains('open')) close();
13
13
  });
14
14
  }
15
- bindModalClose('slim-agent-modal', 'slim-agent-close');
15
+ // Stop the agent-detail "Working for" ticker on every dismiss path so the
16
+ // 1s interval started in openAgentDetail can't leak after the modal closes.
17
+ bindModalClose('slim-agent-modal', 'slim-agent-close', _stopAgentDetailRuntime);
16
18
  bindModalClose('slim-tools-modal', 'slim-tools-close');
17
19
  bindModalClose('slim-tile-modal', 'slim-tile-close');
18
20
 
@@ -249,9 +251,9 @@
249
251
  // Populate + open the cockpit-tile detail modal from the latest status
250
252
  // snapshot. Mirrors the corresponding old-dashboard tab for each tile.
251
253
  function openTileModal(key) {
252
- // The Pinned-context tile has its own list/editor modal (view + edit + unpin)
253
- // rather than the read-only tile detail view.
254
- if (key === 'pinned') { openSlimPinnedList(); return; }
254
+ // The Knowledge tile opens the unified Knowledge control panel (Pinned
255
+ // Context + Notes + KB tabs) rather than the read-only tile detail view.
256
+ if (key === 'knowledge') { openKnowledgeModal(); return; }
255
257
  var view = TILE_VIEWS[key];
256
258
  if (!view) return;
257
259
  var modal = document.getElementById('slim-tile-modal');