@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.
- package/dashboard/js/modal.js +35 -1
- package/dashboard/js/refresh.js +1 -1
- package/dashboard/js/render-agents.js +18 -0
- package/dashboard/js/render-kb.js +27 -2
- package/dashboard/js/render-meetings.js +36 -3
- package/dashboard/js/render-pipelines.js +15 -10
- package/dashboard/js/render-plans.js +73 -3
- package/dashboard/js/render-prd.js +29 -5
- package/dashboard/js/render-prs.js +174 -3
- package/dashboard/js/render-schedules.js +12 -0
- package/dashboard/js/render-utils.js +151 -1
- package/dashboard/js/render-watches.js +61 -7
- package/dashboard/js/render-work-items.js +161 -20
- package/dashboard/js/state.js +110 -1
- package/dashboard/js/utils.js +246 -14
- package/dashboard/layout.html +2 -0
- package/dashboard/slim/body.html +23 -13
- package/dashboard/slim/js/knowledge.js +576 -0
- package/dashboard/slim/js/members.js +43 -0
- package/dashboard/slim/js/modals-tiles.js +6 -4
- package/dashboard/slim/js/pinned.js +7 -19
- package/dashboard/slim/js/status.js +17 -21
- package/dashboard/slim/styles.css +94 -36
- package/dashboard/styles.css +34 -0
- package/dashboard-build.js +1 -1
- package/dashboard.js +50 -2
- package/engine/lifecycle.js +6 -0
- package/engine/pipeline.js +10 -0
- package/engine/queries.js +81 -0
- package/engine/scheduler.js +19 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
253
|
-
// rather than the read-only tile detail view.
|
|
254
|
-
if (key === '
|
|
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');
|