@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.
@@ -80,24 +80,256 @@ function _togglePinAndRefresh(key, source) {
80
80
  else if (source === 'kb') renderKnowledgeBase({ preserveScroll: false });
81
81
  }
82
82
 
83
- // Modal navigation stack — enables back button when opening a modal from another modal
84
- var _modalBackStack = [];
85
- function pushModalBack(reopenFn) {
86
- _modalBackStack.push(reopenFn);
87
- var btn = document.getElementById('modal-back-btn');
88
- if (btn) btn.style.display = '';
83
+ // Modal navigation stack — frames for nested modal layers (P-ce1e5e47)
84
+ // _modalStack holds breadcrumb frames the user can navigate BACK to.
85
+ // _currentVisibleArtifact tracks the currently-shown modal's identity (set by
86
+ // openArtifact / pushModalFrame). Direct row-click openers may leave this null;
87
+ // the back stack still works via the legacy pushModalBack snapshot path.
88
+ //
89
+ // Each frame: { type, id, scrollY, titleHtml, bodyHtml, openFn, openArgs }.
90
+ // Frames whose type starts with "_" (e.g. _legacy, _direct) are opaque
91
+ // breadcrumb-only frames that are NOT mirrored to the URL hash and are NOT
92
+ // replayed on refresh — they exist only for in-session back navigation.
93
+ var MODAL_STACK_MAX_DEPTH = 5;
94
+ var _modalStack = [];
95
+ var _currentVisibleArtifact = null;
96
+ var _legacyFrameSeq = 0;
97
+ var _suppressHistoryFromPopstate = false;
98
+
99
+ function _captureModalSnapshot() {
100
+ var titleEl = document.getElementById('modal-title');
101
+ var bodyEl = document.getElementById('modal-body');
102
+ return {
103
+ titleHtml: titleEl ? titleEl.innerHTML : '',
104
+ bodyHtml: bodyEl ? bodyEl.innerHTML : '',
105
+ scrollY: bodyEl ? (bodyEl.scrollTop || 0) : 0,
106
+ };
107
+ }
108
+ function _modalIsOpen() {
109
+ var el = document.getElementById('modal');
110
+ return !!(el && el.classList.contains('open'));
111
+ }
112
+ function _modalStackDepth() {
113
+ return _modalStack.length + (_currentVisibleArtifact ? 1 : 0);
114
+ }
115
+ function _capModalStackDepth() {
116
+ // Cap at MODAL_STACK_MAX_DEPTH: a 6th push evicts the OLDEST breadcrumb
117
+ // (index 0 of _modalStack), never the visible top.
118
+ while (_modalStackDepth() > MODAL_STACK_MAX_DEPTH && _modalStack.length > 0) {
119
+ _modalStack.shift();
120
+ }
89
121
  }
90
- function modalGoBack() {
91
- var fn = _modalBackStack.pop();
122
+ function _updateModalChrome() {
123
+ var depth = _modalStackDepth();
92
124
  var btn = document.getElementById('modal-back-btn');
93
- if (btn && _modalBackStack.length === 0) btn.style.display = 'none';
94
- if (fn) fn();
125
+ if (btn) btn.style.display = depth > 1 ? '' : 'none';
126
+ var chip = document.getElementById('modal-stack-chip');
127
+ if (chip) {
128
+ if (depth > 1) {
129
+ chip.style.display = '';
130
+ chip.textContent = depth + '/' + MODAL_STACK_MAX_DEPTH;
131
+ } else {
132
+ chip.style.display = 'none';
133
+ }
134
+ }
95
135
  }
96
- function clearModalBackStack() {
97
- _modalBackStack = [];
98
- var btn = document.getElementById('modal-back-btn');
99
- if (btn) btn.style.display = 'none';
136
+ function _buildModalHash() {
137
+ var entries = [];
138
+ for (var i = 0; i < _modalStack.length; i++) {
139
+ var f = _modalStack[i];
140
+ if (f && f.type && f.id && String(f.type).charAt(0) !== '_') {
141
+ entries.push(encodeURIComponent(f.type) + ':' + encodeURIComponent(f.id));
142
+ }
143
+ }
144
+ if (_currentVisibleArtifact && _currentVisibleArtifact.type && _currentVisibleArtifact.id) {
145
+ entries.push(
146
+ encodeURIComponent(_currentVisibleArtifact.type) + ':' +
147
+ encodeURIComponent(_currentVisibleArtifact.id)
148
+ );
149
+ }
150
+ var base = window.location.pathname + window.location.search;
151
+ if (entries.length === 0) return base;
152
+ // Cap URL to last MODAL_STACK_MAX_DEPTH entries (truncate oldest)
153
+ if (entries.length > MODAL_STACK_MAX_DEPTH) entries = entries.slice(-MODAL_STACK_MAX_DEPTH);
154
+ // P-3ed68b1e — frame separator is ',' (not '>'); '>' is in the WHATWG URL
155
+ // fragment percent-encode set and Chromium rewrites it to '%3E' on
156
+ // history.pushState, which broke deep-link replay (split('>') saw zero
157
+ // separators). ',' survives history serialisation intact, and since type
158
+ // + id are individually encodeURIComponent'd (',' itself encodes to
159
+ // '%2C'), no frame field can collide with the separator.
160
+ return base + '#modal=' + entries.join(',');
161
+ }
162
+ function _serializeStackForHistory() {
163
+ return {
164
+ breadcrumbs: _modalStack.map(function(f) { return { type: f.type, id: f.id }; }),
165
+ current: _currentVisibleArtifact
166
+ ? { type: _currentVisibleArtifact.type, id: _currentVisibleArtifact.id }
167
+ : null,
168
+ };
169
+ }
170
+ function _pushModalHistoryState() {
171
+ if (_suppressHistoryFromPopstate) return;
172
+ try {
173
+ var state = { page: (typeof currentPage !== 'undefined' ? currentPage : null), modal: _serializeStackForHistory() };
174
+ history.pushState(state, '', _buildModalHash());
175
+ } catch { /* history disabled (e.g. file://) — fall back to in-memory stack */ }
176
+ }
177
+
178
+ function pushModalFrame(frame) {
179
+ // frame: { type, id, openFn?, openArgs? } — describes the NEW visible modal.
180
+ // The current visible (if any) is snapshotted and pushed onto _modalStack as
181
+ // a breadcrumb; scrollY is captured here so popping restores the user's
182
+ // place in the parent view.
183
+ if (_currentVisibleArtifact) {
184
+ var snap = _captureModalSnapshot();
185
+ _modalStack.push({
186
+ type: _currentVisibleArtifact.type,
187
+ id: _currentVisibleArtifact.id,
188
+ titleHtml: snap.titleHtml,
189
+ bodyHtml: snap.bodyHtml,
190
+ scrollY: snap.scrollY,
191
+ openFn: _currentVisibleArtifact.openFn || null,
192
+ openArgs: _currentVisibleArtifact.openArgs || null,
193
+ });
194
+ } else if (_modalIsOpen()) {
195
+ // Modal was direct-opened (row click) without going through openArtifact.
196
+ // Snapshot it as an opaque _direct breadcrumb so Back still restores it.
197
+ var snap2 = _captureModalSnapshot();
198
+ _modalStack.push({
199
+ type: '_direct',
200
+ id: '_direct_' + (++_legacyFrameSeq),
201
+ titleHtml: snap2.titleHtml,
202
+ bodyHtml: snap2.bodyHtml,
203
+ scrollY: snap2.scrollY,
204
+ openFn: null,
205
+ openArgs: null,
206
+ });
207
+ }
208
+ _currentVisibleArtifact = {
209
+ type: frame.type,
210
+ id: frame.id,
211
+ openFn: typeof frame.openFn === 'function' ? frame.openFn : null,
212
+ openArgs: Array.isArray(frame.openArgs) ? frame.openArgs : null,
213
+ };
214
+ _capModalStackDepth();
215
+ _updateModalChrome();
216
+ _pushModalHistoryState();
217
+ }
218
+
219
+ function popModalFrame() {
220
+ // Esc / X / back-btn / browser Back all funnel here. Use history.back() so
221
+ // the popstate listener is the single source of truth for stack diffing.
222
+ if (_modalStackDepth() === 0) {
223
+ if (_modalIsOpen()) _physicallyCloseModal();
224
+ return;
225
+ }
226
+ try { history.back(); }
227
+ catch { _popOneFrameInternal(); _updateModalChrome(); }
228
+ }
229
+
230
+ function peekTopFrame() {
231
+ if (_currentVisibleArtifact) {
232
+ return Object.assign({}, _currentVisibleArtifact);
233
+ }
234
+ if (_modalStack.length > 0) {
235
+ return Object.assign({}, _modalStack[_modalStack.length - 1]);
236
+ }
237
+ return null;
238
+ }
239
+
240
+ function resetModalStack() {
241
+ _modalStack = [];
242
+ _currentVisibleArtifact = null;
243
+ _updateModalChrome();
244
+ }
245
+
246
+ function withTopFrame(type, id, fn) {
247
+ // Guard for per-modal poll bodies: only invoke `fn` when the named frame is
248
+ // still the visible top, so a stale poll on a lower frame can't clobber the
249
+ // user's currently-stacked modal. When no stack is tracked (direct-opened
250
+ // modal — _currentVisibleArtifact is null and _modalStack is empty) the
251
+ // poll proceeds unconditionally; row-click openers haven't routed through
252
+ // openArtifact, so we have no identity to check.
253
+ var top = peekTopFrame();
254
+ if (top && (top.type !== type || String(top.id) !== String(id))) return false;
255
+ try { fn(); } catch (e) { try { console.error(e); } catch {} }
256
+ return true;
257
+ }
258
+
259
+ function _restoreFrameToVisible(frame) {
260
+ // Synchronously restore a snapshotted frame as the visible modal layer.
261
+ // scrollY is restored after a double-rAF so layout settles first.
262
+ var titleEl = document.getElementById('modal-title');
263
+ var bodyEl = document.getElementById('modal-body');
264
+ if (titleEl) {
265
+ // eslint-disable-next-line no-unsanitized/property -- reason: snapshot of previously-rendered (already-escaped) modal title HTML; round-trip restore only
266
+ titleEl.innerHTML = frame.titleHtml || '';
267
+ }
268
+ if (bodyEl) {
269
+ // eslint-disable-next-line no-unsanitized/property -- reason: snapshot of previously-rendered (already-escaped) modal body HTML; round-trip restore only
270
+ bodyEl.innerHTML = frame.bodyHtml || '';
271
+ }
272
+ var modalEl = document.getElementById('modal');
273
+ if (modalEl && !modalEl.classList.contains('open')) modalEl.classList.add('open');
274
+ requestAnimationFrame(function() {
275
+ requestAnimationFrame(function() {
276
+ var b = document.getElementById('modal-body');
277
+ if (b) b.scrollTop = frame.scrollY || 0;
278
+ });
279
+ });
280
+ }
281
+
282
+ function _popOneFrameInternal() {
283
+ // Pop one breadcrumb and restore it as the visible modal. Used by the
284
+ // popstate handler (and as a fallback when history.back is unavailable).
285
+ if (_modalStack.length === 0) {
286
+ // Nothing to fall back to — close the modal entirely.
287
+ _currentVisibleArtifact = null;
288
+ if (_modalIsOpen()) _physicallyCloseModal();
289
+ return;
290
+ }
291
+ var frame = _modalStack.pop();
292
+ _currentVisibleArtifact = {
293
+ type: frame.type,
294
+ id: frame.id,
295
+ openFn: frame.openFn,
296
+ openArgs: frame.openArgs,
297
+ };
298
+ _restoreFrameToVisible(frame);
299
+ }
300
+
301
+ function _physicallyCloseModal() {
302
+ // Close the modal DOM without going through closeModal() (which would itself
303
+ // call popModalFrame and recurse). Mirrors the visible side-effects of
304
+ // closeModal but skips stack management.
305
+ var modalEl = document.getElementById('modal');
306
+ if (modalEl) modalEl.classList.remove('open');
307
+ var inner = document.querySelector('#modal .modal');
308
+ if (inner) inner.classList.remove('modal-wide');
309
+ }
310
+
311
+ // ─── Deprecation shims (legacy back-stack API) ───────────────────────────────
312
+ // Existing callers in render-work-items.js and render-pipelines.js still use
313
+ // pushModalBack(closure); on Back the closure re-renders the prior modal.
314
+ // We translate this into a breadcrumb push that carries the snapshot of the
315
+ // outgoing modal, so the new popstate-driven Back returns the same view.
316
+ function pushModalBack(reopenFn) {
317
+ var snap = _captureModalSnapshot();
318
+ _modalStack.push({
319
+ type: '_legacy',
320
+ id: '_legacy_' + (++_legacyFrameSeq),
321
+ titleHtml: snap.titleHtml,
322
+ bodyHtml: snap.bodyHtml,
323
+ scrollY: snap.scrollY,
324
+ openFn: typeof reopenFn === 'function' ? reopenFn : null,
325
+ openArgs: null,
326
+ });
327
+ _capModalStackDepth();
328
+ _updateModalChrome();
329
+ _pushModalHistoryState();
100
330
  }
331
+ function modalGoBack() { popModalFrame(); }
332
+ function clearModalBackStack() { resetModalStack(); }
101
333
 
102
334
  function updateModalPinBtn() {
103
335
  var btn = document.getElementById('modal-pin-btn');
@@ -127,6 +127,8 @@
127
127
  <div class="modal-header">
128
128
  <h3 id="modal-title">—</h3>
129
129
  <div class="modal-header-actions">
130
+ <button class="modal-copy modal-back-btn" id="modal-back-btn" onclick="popModalFrame()" title="Back to previous modal" style="display:none"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M9.78 12.78a.75.75 0 01-1.06 0L4.47 8.53a.75.75 0 010-1.06l4.25-4.25a.75.75 0 011.06 1.06L6.06 8l3.72 3.72a.75.75 0 010 1.06z"/></svg> Back</button>
131
+ <span class="modal-stack-chip" id="modal-stack-chip" title="Modal stack depth" style="display:none">1/5</span>
130
132
  <button class="modal-copy" id="modal-edit-btn" onclick="modalToggleEdit()" title="Edit" style="display:none"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M11.013 1.427a1.75 1.75 0 012.474 0l1.086 1.086a1.75 1.75 0 010 2.474l-8.61 8.61c-.21.21-.47.364-.756.445l-3.251.93a.75.75 0 01-.927-.928l.929-3.25c.081-.286.235-.547.445-.758l8.61-8.61zM12.9 2.97L4.288 11.58l-.537 1.878 1.878-.537L14.242 4.31 12.9 2.97z"/></svg> Edit</button>
131
133
  <button class="modal-copy is-success" id="modal-save-btn" onclick="modalSaveEdit()" title="Save" style="display:none"><svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/></svg> Save</button>
132
134
  <button class="modal-copy" id="modal-cancel-edit-btn" onclick="modalCancelEdit()" title="Cancel edit" style="display:none">Cancel</button>
@@ -106,11 +106,11 @@
106
106
  <div class="cockpit-value dim">0</div>
107
107
  <div class="cockpit-detail">no watches set</div>
108
108
  </div>
109
- <div class="cockpit-tile" data-tile="pinned">
109
+ <div class="cockpit-tile" data-tile="knowledge">
110
110
  <button id="slim-tile-pin-chip" class="linkpr-chip on-tile" type="button" title="Pin content">+ Pin</button>
111
- <div class="cockpit-label"><span class="cockpit-dot"></span> Pinned context</div>
111
+ <div class="cockpit-label"><span class="cockpit-dot"></span> Knowledge</div>
112
112
  <div class="cockpit-value dim">0</div>
113
- <div class="cockpit-detail">nothing pinned</div>
113
+ <div class="cockpit-detail">pinned context, notes &amp; KB</div>
114
114
  </div>
115
115
  </div>
116
116
  </div>
@@ -187,18 +187,28 @@
187
187
  </div>
188
188
  </div>
189
189
 
190
- <!-- Pinned context list — opened from the "Pinned context" status tile. Lists
191
- all pinned notes with view/edit/unpin; "+ Pin content" opens the editor.
192
- Backed by /api/pinned (GET via /api/status), /api/pinned/update, /remove.
193
- Wired in dashboard/slim/js/pinned.js. -->
194
- <div class="modal-bg" id="slim-pinned-modal">
195
- <div class="modal">
190
+ <!-- Knowledge control panel — opened from the "Knowledge" status tile. One
191
+ unified box surfacing all three knowledge surfaces via tabs: Pinned
192
+ Context, Notes, and KB (knowledge base). Each tab is rendered lazily by
193
+ dashboard/slim/js/knowledge.js into #slim-knowledge-body. Backed by the
194
+ existing endpoints (no new server routes):
195
+ Pinned — /api/pinned (+ update/remove), surfaced via /api/status.
196
+ Notes — /api/notes-full (read), /api/notes-save (edit notes.md),
197
+ /api/notes (add an inbox note).
198
+ KB — /api/knowledge (list), /api/knowledge/:cat/:file (read),
199
+ /api/knowledge POST (create), /api/kb-pins(+/toggle). -->
200
+ <div class="modal-bg" id="slim-knowledge-modal">
201
+ <div class="modal slim-knowledge-modal-inner">
196
202
  <div class="modal-header">
197
- <h3>Pinned context</h3>
198
- <button id="slim-pinned-add" class="linkpr-chip" type="button" style="margin-left:auto;margin-right:8px">+ Pin content</button>
199
- <button id="slim-pinned-close" class="icon-btn" title="Close">&times;</button>
203
+ <h3>Knowledge</h3>
204
+ <div class="kn-tabs" id="slim-kn-tabs" role="tablist">
205
+ <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>
206
+ <button class="kn-tab" id="slim-kn-tab-notes" data-kn-tab="notes" type="button" role="tab" aria-selected="false" aria-controls="slim-knowledge-body">Notes</button>
207
+ <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>
208
+ </div>
209
+ <button id="slim-knowledge-close" class="icon-btn" title="Close" style="margin-left:auto">&times;</button>
200
210
  </div>
201
- <div class="modal-body" id="slim-pinned-body"></div>
211
+ <div class="modal-body" id="slim-knowledge-body" role="tabpanel" tabindex="0" aria-labelledby="slim-kn-tab-pinned"></div>
202
212
  </div>
203
213
  </div>
204
214