@yemi33/minions 0.1.2439 → 0.1.2441
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/settings.js +21 -0
- package/dashboard/slim/body.html +11 -11
- package/dashboard/slim/js/knowledge.js +92 -459
- package/dashboard/slim/js/settings.js +82 -8
- package/dashboard/slim/styles.css +16 -47
- package/package.json +1 -1
package/dashboard/js/settings.js
CHANGED
|
@@ -1960,4 +1960,25 @@ window.MinionsSettings = { openSettings, saveSettings, addProject, removeProject
|
|
|
1960
1960
|
} else {
|
|
1961
1961
|
setTimeout(openWhenReady, 0);
|
|
1962
1962
|
}
|
|
1963
|
+
// FRESHNESS ON REOPEN (W-ms4ogdc800aa6931): the slim parent now CACHES this
|
|
1964
|
+
// embedded /settings frame (created once, shown/hidden) instead of cold-booting
|
|
1965
|
+
// it per open, so the iframe `load` — and thus the one-shot openSettings() boot
|
|
1966
|
+
// above — fires only ONCE. Re-pull /api/settings + /api/features whenever the
|
|
1967
|
+
// parent re-shows us (visible:true) so a value changed elsewhere isn't stale. A
|
|
1968
|
+
// full openSettings() re-render is cheap here (the SPA is already booted — just
|
|
1969
|
+
// two localhost fetches + a DOM render, no cold boot), so we prefer it over
|
|
1970
|
+
// ad-hoc partial refresh. Reuses the SAME {source:'minions-slim-embed',
|
|
1971
|
+
// type:'visibility'} channel refresh.js already honors (no second postMessage
|
|
1972
|
+
// bus). Embed-mode gated by the isSettingsPath+isEmbed early return above, so a
|
|
1973
|
+
// standalone /settings visit is completely unaffected.
|
|
1974
|
+
try {
|
|
1975
|
+
window.addEventListener('message', function(ev) {
|
|
1976
|
+
if (!ev || ev.origin !== window.location.origin) return;
|
|
1977
|
+
var d = ev.data;
|
|
1978
|
+
if (!d || d.source !== 'minions-slim-embed' || d.type !== 'visibility') return;
|
|
1979
|
+
if (d.visible && typeof openSettings === 'function') {
|
|
1980
|
+
try { openSettings(); } catch (e) { /* transient — next show retries */ }
|
|
1981
|
+
}
|
|
1982
|
+
});
|
|
1983
|
+
} catch (e) { /* defensive — never block the embed boot on this hook */ }
|
|
1963
1984
|
})();
|
package/dashboard/slim/body.html
CHANGED
|
@@ -202,24 +202,24 @@
|
|
|
202
202
|
</div>
|
|
203
203
|
</div>
|
|
204
204
|
|
|
205
|
-
<!-- Knowledge control panel — opened from the "Knowledge" status tile.
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
205
|
+
<!-- Knowledge control panel — opened from the "Knowledge" status tile. Two
|
|
206
|
+
tabs: Pinned Context (slim-native, via pinned.js — there is no standalone
|
|
207
|
+
classic route for pinned, it lives on /home) and Notes & Knowledge, which
|
|
208
|
+
embeds the LITERAL classic /inbox screen (Notes Inbox + Team Notes +
|
|
209
|
+
Knowledge Base + memory search) in a chrome-off /inbox?embed=1 iframe
|
|
210
|
+
rather than a slim-native fork (CLAUDE.md "Slim UX vs Classic UX — reuse,
|
|
211
|
+
don't fork"). Rendered lazily by dashboard/slim/js/knowledge.js into
|
|
212
|
+
#slim-knowledge-body. Backed by the existing endpoints (no new routes):
|
|
210
213
|
Pinned — /api/pinned (+ update/remove), surfaced via /api/status.
|
|
211
|
-
Notes
|
|
212
|
-
/api/
|
|
213
|
-
KB — /api/knowledge (list), /api/knowledge/:cat/:file (read),
|
|
214
|
-
/api/knowledge POST (create), /api/kb-pins(+/toggle). -->
|
|
214
|
+
Notes & Knowledge — the classic /inbox page (/api/notes*, /api/knowledge,
|
|
215
|
+
/api/kb-pins, memory search) inside the embedded iframe. -->
|
|
215
216
|
<div class="modal-bg" id="slim-knowledge-modal">
|
|
216
217
|
<div class="modal slim-knowledge-modal-inner">
|
|
217
218
|
<div class="modal-header">
|
|
218
219
|
<h3>Knowledge</h3>
|
|
219
220
|
<div class="kn-tabs" id="slim-kn-tabs" role="tablist">
|
|
220
221
|
<button class="kn-tab active" id="slim-kn-tab-pinned" data-kn-tab="pinned" type="button" role="tab" aria-selected="true" aria-controls="slim-knowledge-body">Pinned Context</button>
|
|
221
|
-
<button class="kn-tab" id="slim-kn-tab-
|
|
222
|
-
<button class="kn-tab" id="slim-kn-tab-kb" data-kn-tab="kb" type="button" role="tab" aria-selected="false" aria-controls="slim-knowledge-body">KB</button>
|
|
222
|
+
<button class="kn-tab" id="slim-kn-tab-inbox" data-kn-tab="inbox" type="button" role="tab" aria-selected="false" aria-controls="slim-knowledge-body">Notes & Knowledge</button>
|
|
223
223
|
</div>
|
|
224
224
|
<button id="slim-knowledge-close" class="icon-btn" title="Close" style="margin-left:auto">×</button>
|
|
225
225
|
</div>
|
|
@@ -1,34 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
// ── Knowledge control panel (Pinned Context + Notes
|
|
3
|
-
// One
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// Notes
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
2
|
+
// ── Knowledge control panel (Pinned Context + Notes & Knowledge) ────
|
|
3
|
+
// One cockpit box over the knowledge surfaces, replacing the old single
|
|
4
|
+
// "Pinned context" tile. Two tabs:
|
|
5
|
+
// Pinned — slim-native (there is no standalone classic route for pinned;
|
|
6
|
+
// it lives on /home). Reuses the editor + list renderer in
|
|
7
|
+
// pinned.js, backed by /api/pinned (+ update/remove).
|
|
8
|
+
// Notes & Knowledge — embeds the LITERAL classic /inbox screen (Notes
|
|
9
|
+
// Inbox + Team Notes + Knowledge Base + memory search) in a
|
|
10
|
+
// chrome-off /inbox?embed=1 iframe, per CLAUDE.md "Slim UX vs
|
|
11
|
+
// Classic UX — reuse, don't fork". Zero forked rendering logic.
|
|
12
12
|
//
|
|
13
13
|
// Counts: pinned is live from the 5s status poll (renderKnowledgeTile is fed
|
|
14
14
|
// by applyStatus). Notes + KB counts are fetched lazily — once shortly after
|
|
15
|
-
// load and again
|
|
15
|
+
// load and again on modal open — so the heavier /api/notes-full and
|
|
16
16
|
// /api/knowledge reads stay off the recurring poll.
|
|
17
17
|
|
|
18
18
|
var _knowledgeCounts = { pinned: 0, notes: null, kb: null };
|
|
19
19
|
var _knActiveTab = 'pinned';
|
|
20
|
-
var
|
|
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
|
|
20
|
+
var _knInboxFrame = null; // cached /inbox?embed=1 iframe (created once)
|
|
32
21
|
|
|
33
22
|
// ── Cockpit tile ───────────────────────────────────────────────────
|
|
34
23
|
// Called from applyStatus with the live pinned count each poll, and from the
|
|
@@ -68,35 +57,24 @@
|
|
|
68
57
|
try {
|
|
69
58
|
var kres = await fetch('/api/knowledge', { headers: { 'Accept': 'application/json' } });
|
|
70
59
|
if (kres.ok) {
|
|
71
|
-
|
|
72
|
-
_knowledgeCounts.kb = _countKbEntries(_kbData);
|
|
60
|
+
_knowledgeCounts.kb = _countKbEntries(await kres.json());
|
|
73
61
|
}
|
|
74
62
|
} catch (e) { /* keep last-known */ }
|
|
75
63
|
renderKnowledgeTile();
|
|
76
64
|
}
|
|
77
65
|
|
|
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
66
|
// ── Modal open/close + tab switching ───────────────────────────────
|
|
91
67
|
function openKnowledgeModal() {
|
|
92
68
|
var modal = document.getElementById('slim-knowledge-modal');
|
|
93
69
|
if (!modal) return;
|
|
94
|
-
renderKnowledgeTab();
|
|
70
|
+
renderKnowledgeTab(); // posts visibility=true to the embed if the inbox tab is active
|
|
95
71
|
modal.classList.add('open');
|
|
96
72
|
}
|
|
97
73
|
function closeKnowledgeModal() {
|
|
98
74
|
var modal = document.getElementById('slim-knowledge-modal');
|
|
99
75
|
if (modal) modal.classList.remove('open');
|
|
76
|
+
// Suspend the embedded /inbox poll while the modal is closed.
|
|
77
|
+
_postKnEmbedVisibility(false);
|
|
100
78
|
}
|
|
101
79
|
function setKnowledgeTab(tab) {
|
|
102
80
|
_knActiveTab = tab;
|
|
@@ -112,13 +90,81 @@
|
|
|
112
90
|
if (body && activeTabId) body.setAttribute('aria-labelledby', activeTabId);
|
|
113
91
|
renderKnowledgeTab();
|
|
114
92
|
}
|
|
93
|
+
// Tab routing. The Pinned tab renders slim-native content into a transient
|
|
94
|
+
// container; the Notes & Knowledge tab shows a cached /inbox?embed=1 iframe
|
|
95
|
+
// (created once, kept mounted so a tab switch never reloads it).
|
|
115
96
|
function renderKnowledgeTab() {
|
|
97
|
+
var c = _ensureKnContainers();
|
|
98
|
+
if (!c) return;
|
|
99
|
+
if (_knActiveTab === 'inbox') {
|
|
100
|
+
renderKnowledgeInboxTab();
|
|
101
|
+
} else {
|
|
102
|
+
c.host.hidden = true;
|
|
103
|
+
_postKnEmbedVisibility(false);
|
|
104
|
+
c.transient.textContent = '';
|
|
105
|
+
renderKnowledgePinnedTab(c.transient);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Persistent containers inside #slim-knowledge-body: an embed host that holds
|
|
110
|
+
// the cached iframe (never wiped, so the iframe is never re-parented/reloaded)
|
|
111
|
+
// and a transient container for the Pinned tab's slim-native content. Built
|
|
112
|
+
// with createElement (no innerHTML) to satisfy the no-unsanitized lint gate.
|
|
113
|
+
function _ensureKnContainers() {
|
|
116
114
|
var body = document.getElementById('slim-knowledge-body');
|
|
117
|
-
if (!body) return;
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
if (!body) return null;
|
|
116
|
+
var host = document.getElementById('slim-kn-embed-host');
|
|
117
|
+
if (!host) {
|
|
118
|
+
host = document.createElement('div');
|
|
119
|
+
host.id = 'slim-kn-embed-host';
|
|
120
|
+
host.className = 'slim-kn-embed-host';
|
|
121
|
+
host.hidden = true;
|
|
122
|
+
body.appendChild(host);
|
|
123
|
+
}
|
|
124
|
+
var transient = document.getElementById('slim-kn-transient');
|
|
125
|
+
if (!transient) {
|
|
126
|
+
transient = document.createElement('div');
|
|
127
|
+
transient.id = 'slim-kn-transient';
|
|
128
|
+
transient.className = 'slim-kn-transient';
|
|
129
|
+
body.appendChild(transient);
|
|
130
|
+
}
|
|
131
|
+
return { host: host, transient: transient };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Tell the embedded classic /inbox SPA whether its iframe is visible so it can
|
|
135
|
+
// suspend/resume its own /api/status poll (dashboard/js/refresh.js listens for
|
|
136
|
+
// this exact message shape). Same-origin, guarded defensively.
|
|
137
|
+
function _postKnEmbedVisibility(visible) {
|
|
138
|
+
try {
|
|
139
|
+
if (_knInboxFrame && _knInboxFrame.contentWindow) {
|
|
140
|
+
_knInboxFrame.contentWindow.postMessage(
|
|
141
|
+
{ source: 'minions-slim-embed', type: 'visibility', visible: !!visible }, '*');
|
|
142
|
+
}
|
|
143
|
+
} catch (e) { /* defensive — same-origin means this should never throw */ }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Tab: Notes & Knowledge (classic /inbox embed) ──────────────────
|
|
147
|
+
// Lazily creates ONE cached iframe pointing at the classic Notes+KB screen
|
|
148
|
+
// (/inbox?embed=1) and mounts it into the persistent embed host. Subsequent
|
|
149
|
+
// activations just re-show the same iframe (never recreated), so switching
|
|
150
|
+
// tabs never reloads it.
|
|
151
|
+
function renderKnowledgeInboxTab() {
|
|
152
|
+
var c = _ensureKnContainers();
|
|
153
|
+
if (!c) return;
|
|
154
|
+
c.transient.textContent = '';
|
|
155
|
+
if (!_knInboxFrame) {
|
|
156
|
+
var frame = document.createElement('iframe');
|
|
157
|
+
frame.className = 'slim-kn-embed';
|
|
158
|
+
frame.src = '/inbox?embed=1';
|
|
159
|
+
frame.title = 'Notes & Knowledge';
|
|
160
|
+
frame.addEventListener('load', function() {
|
|
161
|
+
_postKnEmbedVisibility(_knActiveTab === 'inbox');
|
|
162
|
+
});
|
|
163
|
+
c.host.appendChild(frame);
|
|
164
|
+
_knInboxFrame = frame;
|
|
165
|
+
}
|
|
166
|
+
c.host.hidden = false;
|
|
167
|
+
_postKnEmbedVisibility(true);
|
|
122
168
|
}
|
|
123
169
|
|
|
124
170
|
// ── Tab: Pinned Context ────────────────────────────────────────────
|
|
@@ -143,423 +189,10 @@
|
|
|
143
189
|
renderSlimPinnedList(); // defined in pinned.js; renders into #slim-pinned-list
|
|
144
190
|
}
|
|
145
191
|
|
|
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
192
|
// ── Wiring ─────────────────────────────────────────────────────────
|
|
562
|
-
|
|
193
|
+
// onClose fires on every dismiss path (close-X, backdrop, Esc) so the
|
|
194
|
+
// embedded /inbox poll is suspended whenever the modal closes.
|
|
195
|
+
bindModalClose('slim-knowledge-modal', 'slim-knowledge-close', function() { _postKnEmbedVisibility(false); });
|
|
563
196
|
(function bindKnowledgeUi() {
|
|
564
197
|
var tabBar = document.getElementById('slim-kn-tabs');
|
|
565
198
|
if (tabBar) {
|
|
@@ -18,16 +18,44 @@
|
|
|
18
18
|
// — two stacked "Settings" bars was the duplicate-menu-bar bug
|
|
19
19
|
// (W-mrpbzke4000ie609). Closing is driven from the embedded modal's own close
|
|
20
20
|
// X (wired below to the parent), plus backdrop-click and Escape.
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
// CACHED settings iframe (W-ms4ogdc800aa6931). The old code discarded the
|
|
22
|
+
// frame on close and appended a BRAND-NEW `/settings?embed=1` iframe on every
|
|
23
|
+
// open, so each open cold-booted the entire classic SPA + full Settings render
|
|
24
|
+
// (/api/settings + /api/features + per-agent runtime dropdowns + projects +
|
|
25
|
+
// routing, plus the SPA's 4s /api/status poll) — the multi-second stall. We
|
|
26
|
+
// now mirror the cockpit-tile reuse contract (modals-tiles.js
|
|
27
|
+
// #_getOrCreateEmbedFrame / _showReusableEmbed): create the frame ONCE, keep
|
|
28
|
+
// it mounted (hidden), and show/hide it. A single background boot replaces N
|
|
29
|
+
// per-open cold boots; while hidden the embedded SPA's poll is suspended via
|
|
30
|
+
// the shared {source:'minions-slim-embed',type:'visibility'} message
|
|
31
|
+
// (refresh.js honors it in embed mode) so the cached frame doesn't poll
|
|
32
|
+
// /api/status forever off-screen.
|
|
33
|
+
var _slimSettingsFrame = null;
|
|
34
|
+
|
|
35
|
+
// Create (once) the cached settings iframe and return it. Built with
|
|
36
|
+
// createElement (no innerHTML) to satisfy the dashboard no-unsanitized lint
|
|
37
|
+
// gate. Starts hidden; a load-time visibility message suspends its poll until
|
|
38
|
+
// it is shown. Because the frame is cached, its `load` handler fires exactly
|
|
39
|
+
// ONCE — so the close/Escape wiring is installed here and persists across all
|
|
40
|
+
// hide/show cycles.
|
|
41
|
+
function _getOrCreateSettingsFrame() {
|
|
42
|
+
if (_slimSettingsFrame) return _slimSettingsFrame;
|
|
23
43
|
var body = document.getElementById('slim-settings-body');
|
|
24
|
-
if (!
|
|
25
|
-
body.textContent = '';
|
|
44
|
+
if (!body) return null;
|
|
45
|
+
body.textContent = ''; // drop the "Loading settings…" placeholder (once)
|
|
26
46
|
var frame = document.createElement('iframe');
|
|
27
47
|
frame.className = 'slim-settings-embed';
|
|
28
48
|
frame.src = '/settings?embed=1';
|
|
29
49
|
frame.title = 'Settings';
|
|
50
|
+
frame.hidden = true;
|
|
30
51
|
frame.addEventListener('load', function() {
|
|
52
|
+
// Suspend/resume the embedded SPA poll to match current visibility. On a
|
|
53
|
+
// visible:true the embedded classic Settings also re-pulls /api/settings +
|
|
54
|
+
// /api/features (see dashboard/js/settings.js#bootSettingsEmbed) — the
|
|
55
|
+
// same single postMessage channel, no second bus.
|
|
56
|
+
var modal = document.getElementById('slim-settings-modal');
|
|
57
|
+
var shown = !!(modal && modal.classList.contains('open'));
|
|
58
|
+
if (typeof _postEmbedVisibility === 'function') _postEmbedVisibility(frame, shown);
|
|
31
59
|
var frameDocument = frame.contentDocument;
|
|
32
60
|
if (!frameDocument) return;
|
|
33
61
|
// Capture before the classic modal handler so only the parent modal closes.
|
|
@@ -53,20 +81,50 @@
|
|
|
53
81
|
}
|
|
54
82
|
});
|
|
55
83
|
body.appendChild(frame);
|
|
84
|
+
_slimSettingsFrame = frame;
|
|
85
|
+
return frame;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function openSlimSettings() {
|
|
89
|
+
var modal = document.getElementById('slim-settings-modal');
|
|
90
|
+
var frame = _getOrCreateSettingsFrame();
|
|
91
|
+
if (!modal || !frame) return;
|
|
92
|
+
frame.hidden = false;
|
|
93
|
+
// Resume the embedded SPA poll + trigger a fresh /api/settings+/api/features
|
|
94
|
+
// re-pull (freshness on reopen — the cached frame's `load` fired only once,
|
|
95
|
+
// so without this a value changed elsewhere would show stale).
|
|
96
|
+
if (typeof _postEmbedVisibility === 'function') _postEmbedVisibility(frame, true);
|
|
56
97
|
modal.classList.add('open');
|
|
57
98
|
}
|
|
58
99
|
|
|
59
100
|
function closeSlimSettings() {
|
|
60
|
-
document.getElementById('slim-settings-modal')
|
|
101
|
+
var modal = document.getElementById('slim-settings-modal');
|
|
102
|
+
if (!modal) return;
|
|
103
|
+
// Keep the frame MOUNTED for an instant reopen; just hide it and suspend its
|
|
104
|
+
// poll (so the cached frame stops polling /api/status in the background).
|
|
105
|
+
if (_slimSettingsFrame) {
|
|
106
|
+
if (typeof _postEmbedVisibility === 'function') _postEmbedVisibility(_slimSettingsFrame, false);
|
|
107
|
+
_slimSettingsFrame.hidden = true;
|
|
108
|
+
}
|
|
109
|
+
modal.classList.remove('open');
|
|
61
110
|
}
|
|
62
111
|
|
|
63
112
|
(function bindSettingsUi() {
|
|
64
113
|
var btn = document.getElementById('slim-settings-btn');
|
|
65
114
|
var backdrop = document.getElementById('slim-settings-modal');
|
|
66
|
-
if (btn)
|
|
115
|
+
if (btn) {
|
|
116
|
+
btn.addEventListener('click', openSlimSettings);
|
|
117
|
+
// PREDICTIVE prewarm: the first hover / focus / press of the gear boots the
|
|
118
|
+
// cached settings frame before the click lands, so the open is an instant
|
|
119
|
+
// show. Fires at most once (the frame is cached thereafter).
|
|
120
|
+
var predictivePrewarm = function() { _getOrCreateSettingsFrame(); };
|
|
121
|
+
btn.addEventListener('pointerenter', predictivePrewarm, { once: true });
|
|
122
|
+
btn.addEventListener('pointerdown', predictivePrewarm, { once: true });
|
|
123
|
+
btn.addEventListener('focus', predictivePrewarm, { once: true });
|
|
124
|
+
}
|
|
67
125
|
// No outer close button: the embedded classic Settings modal's own close X
|
|
68
|
-
// (wired to the parent in
|
|
69
|
-
// alongside backdrop-click and Escape below (W-mrpbzke4000ie609).
|
|
126
|
+
// (wired to the parent in _getOrCreateSettingsFrame) is the single close
|
|
127
|
+
// affordance, alongside backdrop-click and Escape below (W-mrpbzke4000ie609).
|
|
70
128
|
if (backdrop) {
|
|
71
129
|
backdrop.addEventListener('click', function(ev) {
|
|
72
130
|
if (ev.target === backdrop) closeSlimSettings();
|
|
@@ -78,6 +136,22 @@
|
|
|
78
136
|
});
|
|
79
137
|
})();
|
|
80
138
|
|
|
139
|
+
// Deferred prewarm: boot the cached settings frame AFTER the cockpit's own hot
|
|
140
|
+
// embed prewarm (modals-tiles.js schedules that at ~800ms) so it never competes
|
|
141
|
+
// with slim cockpit initial paint. Idle-callback when available, else a delayed
|
|
142
|
+
// timeout — either way it runs well after first paint. First open then becomes
|
|
143
|
+
// an instant show instead of a cold boot.
|
|
144
|
+
(function scheduleSettingsPrewarm() {
|
|
145
|
+
var warm = function() { try { _getOrCreateSettingsFrame(); } catch (e) { /* skip prewarm */ } };
|
|
146
|
+
try {
|
|
147
|
+
if (typeof requestIdleCallback === 'function') {
|
|
148
|
+
requestIdleCallback(function() { setTimeout(warm, 1200); }, { timeout: 4000 });
|
|
149
|
+
} else {
|
|
150
|
+
setTimeout(warm, 1600);
|
|
151
|
+
}
|
|
152
|
+
} catch (e) { /* no timers available — skip prewarm, first open cold-boots */ }
|
|
153
|
+
})();
|
|
154
|
+
|
|
81
155
|
// POST a feature-flag toggle; throws on a non-2xx so callers can branch on
|
|
82
156
|
// failure. Used by the one-click returnToClassicDashboard().
|
|
83
157
|
async function postFeatureToggle(id, enabled) {
|
|
@@ -797,6 +797,12 @@
|
|
|
797
797
|
outranks the class rules (no !important needed). */
|
|
798
798
|
.slim-tile-embed-host, .slim-tile-transient { display: block; }
|
|
799
799
|
#slim-tile-embed-host iframe[hidden] { display: none; }
|
|
800
|
+
/* Cached slim Settings frame (W-ms4ogdc800aa6931): like the tile embeds it is
|
|
801
|
+
created once and shown/hidden rather than cold-booted per open. The
|
|
802
|
+
.slim-settings-embed class rule above sets display:block, which outranks
|
|
803
|
+
[hidden]'s UA display:none — so a hidden cached frame must be forced back
|
|
804
|
+
to none. The id+attr selector outranks the class rule (no !important). */
|
|
805
|
+
#slim-settings-body iframe[hidden] { display: none; }
|
|
800
806
|
/* Automation tile (Watches · Schedules · Pipelines) — a tabbed composite
|
|
801
807
|
inside the shared wide tile-modal. The tab bar sits above the embedded
|
|
802
808
|
classic screen, so the iframe height leaves room for it (vs the padless
|
|
@@ -867,8 +873,9 @@
|
|
|
867
873
|
.pinned-row-actions .btn-secondary { padding: 4px 12px; font-size: var(--text-base); }
|
|
868
874
|
.pinned-row-unpin { color: var(--red); border-color: var(--red); }
|
|
869
875
|
|
|
870
|
-
/* Knowledge control panel (slim-knowledge-modal): Pinned Context
|
|
871
|
-
|
|
876
|
+
/* Knowledge control panel (slim-knowledge-modal): Pinned Context (slim-
|
|
877
|
+
native) + Notes & Knowledge (embedded classic /inbox) tabs in one box.
|
|
878
|
+
Reuses .tile-* and .pinned-row primitives. */
|
|
872
879
|
.slim-knowledge-modal-inner { width: 720px; max-width: calc(100vw - 32px); }
|
|
873
880
|
.kn-tabs { display: flex; gap: 6px; margin-left: 16px; }
|
|
874
881
|
.kn-tab {
|
|
@@ -899,51 +906,13 @@
|
|
|
899
906
|
}
|
|
900
907
|
.kn-msg { font-size: var(--text-base); min-height: 16px; color: var(--muted); }
|
|
901
908
|
.kn-msg-inline { margin-right: auto; }
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
}
|
|
910
|
-
/* KB list rows */
|
|
911
|
-
.kb-row {
|
|
912
|
-
border: 1px solid var(--border);
|
|
913
|
-
border-radius: var(--radius);
|
|
914
|
-
background: var(--surface2);
|
|
915
|
-
padding: 8px 10px;
|
|
916
|
-
margin-bottom: 8px;
|
|
917
|
-
cursor: pointer;
|
|
918
|
-
}
|
|
919
|
-
.kb-row:hover { border-color: var(--blue); }
|
|
920
|
-
.kb-row-pinned { border-color: var(--amber); }
|
|
921
|
-
.kb-row-top { display: flex; align-items: center; gap: 8px; }
|
|
922
|
-
.kb-row-title {
|
|
923
|
-
flex: 1; min-width: 0;
|
|
924
|
-
font-weight: 600; font-size: var(--text-md); color: var(--text);
|
|
925
|
-
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
926
|
-
}
|
|
927
|
-
.kb-pin-btn {
|
|
928
|
-
flex: 0 0 auto;
|
|
929
|
-
border: none; background: transparent; cursor: pointer;
|
|
930
|
-
color: var(--muted); font-size: var(--text-lg); line-height: 1; padding: 0 2px;
|
|
931
|
-
}
|
|
932
|
-
.kb-pin-btn.on { color: var(--amber); }
|
|
933
|
-
.kb-pin-btn:hover { color: var(--amber); }
|
|
934
|
-
.kb-row-meta { font-size: var(--text-base); color: var(--muted); margin-top: 3px; }
|
|
935
|
-
.kb-row-preview {
|
|
936
|
-
margin-top: 5px;
|
|
937
|
-
font-size: var(--text-base); color: var(--muted); line-height: 1.5;
|
|
938
|
-
word-break: break-word;
|
|
939
|
-
}
|
|
940
|
-
.kb-entry-content {
|
|
941
|
-
white-space: pre-wrap; word-break: break-word;
|
|
942
|
-
font-family: var(--mono, monospace); font-size: var(--text-base);
|
|
943
|
-
color: var(--text); line-height: 1.55;
|
|
944
|
-
background: var(--surface2); border: 1px solid var(--border);
|
|
945
|
-
border-radius: var(--radius); padding: 12px; margin: 0;
|
|
946
|
-
max-height: 60vh; overflow-y: auto;
|
|
909
|
+
/* Notes & Knowledge tab: the embedded classic /inbox screen. The host div
|
|
910
|
+
is toggled via the [hidden] attribute — a plain div defaults to
|
|
911
|
+
display:block when shown and the UA [hidden] rule forces display:none
|
|
912
|
+
when hidden, so no extra host rule is needed. */
|
|
913
|
+
.slim-kn-embed {
|
|
914
|
+
display: block; width: 100%; height: 62vh; min-height: 360px;
|
|
915
|
+
border: 0; background: var(--bg);
|
|
947
916
|
}
|
|
948
917
|
|
|
949
918
|
.chat-thinking {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yemi33/minions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2441",
|
|
4
4
|
"description": "Multi-agent AI dev team that runs from ~/.minions/ — five autonomous agents share a single engine, dashboard, and knowledge base",
|
|
5
5
|
"bin": {
|
|
6
6
|
"minions": "bin/minions.js"
|