@yemi33/minions 0.1.2194 → 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/command-center.js +2 -0
- package/dashboard/js/fre.js +125 -1
- package/dashboard/js/modal.js +35 -1
- package/dashboard/js/refresh.js +7 -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
|
@@ -476,6 +476,8 @@ function ccAbort() {
|
|
|
476
476
|
|
|
477
477
|
function toggleCommandCenter() {
|
|
478
478
|
_ccOpen = !_ccOpen;
|
|
479
|
+
// Opening CC means the operator found it — retire the first-use coachmark.
|
|
480
|
+
if (_ccOpen && typeof dismissCcHint === 'function') dismissCcHint();
|
|
479
481
|
var drawer = document.getElementById('cc-drawer');
|
|
480
482
|
var overlay = document.getElementById('cc-overlay');
|
|
481
483
|
if (_ccOpen) ccApplySavedWidth();
|
package/dashboard/js/fre.js
CHANGED
|
@@ -188,4 +188,128 @@ function renderFre(statusOrProjects) {
|
|
|
188
188
|
'</div>';
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
// ── Command Center first-use coachmark ──────────────────────────────────────
|
|
192
|
+
// A small popup anchored under the Command Center header button, shown to
|
|
193
|
+
// brand-new operators who have never used CC. Teaches the primary entry point:
|
|
194
|
+
// "prompt the Command Center to ask it to dispatch work." Self-contained and
|
|
195
|
+
// idempotent — safe to call renderCcHint() every refresh tick.
|
|
196
|
+
//
|
|
197
|
+
// Trigger: NOT dismissed AND the operator has never sent a CC message (no
|
|
198
|
+
// 'cc-tabs' entry carries any messages). Auto-suppressed while the CC drawer
|
|
199
|
+
// is open. Dismissed permanently when the user opens CC (see
|
|
200
|
+
// toggleCommandCenter in command-center.js) or clicks "Got it".
|
|
201
|
+
|
|
202
|
+
const CC_HINT_DISMISS_KEY = 'minions_cc_hint_dismissed';
|
|
203
|
+
const CC_HINT_MOUNT_ID = 'cc-hint-coachmark';
|
|
204
|
+
|
|
205
|
+
function _ccHintIsDismissed() {
|
|
206
|
+
try { return localStorage.getItem(CC_HINT_DISMISS_KEY) === '1'; } catch { return false; }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// "Brand new" = the operator has never sent a Command Center message. Reads the
|
|
210
|
+
// same 'cc-tabs' localStorage key command-center.js persists; any tab carrying
|
|
211
|
+
// at least one message means CC has been used and the hint has served its
|
|
212
|
+
// purpose. Absent/empty/parse-error all read as new (fail toward showing).
|
|
213
|
+
function _ccHintUserIsNew() {
|
|
214
|
+
try {
|
|
215
|
+
const raw = localStorage.getItem('cc-tabs');
|
|
216
|
+
if (!raw) return true;
|
|
217
|
+
const tabs = JSON.parse(raw);
|
|
218
|
+
if (!Array.isArray(tabs) || tabs.length === 0) return true;
|
|
219
|
+
return !tabs.some(function(t) { return t && Array.isArray(t.messages) && t.messages.length > 0; });
|
|
220
|
+
} catch { return true; }
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function _ccHintRemove() {
|
|
224
|
+
const mount = document.getElementById(CC_HINT_MOUNT_ID);
|
|
225
|
+
if (mount && mount.parentNode) mount.parentNode.removeChild(mount);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function dismissCcHint() {
|
|
229
|
+
try { localStorage.setItem(CC_HINT_DISMISS_KEY, '1'); } catch { /* expected */ }
|
|
230
|
+
_ccHintRemove();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Reset helper (exposed for tests / debugging only). Not wired to UI.
|
|
234
|
+
function _ccHintReset() {
|
|
235
|
+
try { localStorage.removeItem(CC_HINT_DISMISS_KEY); } catch { /* expected */ }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Open the Command Center from the coachmark CTA, then dismiss the hint.
|
|
239
|
+
function openCcFromHint() {
|
|
240
|
+
dismissCcHint();
|
|
241
|
+
if (typeof toggleCommandCenter === 'function' && !window._ccOpen) {
|
|
242
|
+
try { toggleCommandCenter(); } catch { /* drawer not mounted yet */ }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Re-anchor the coachmark under the CC button. Called on render and on resize.
|
|
247
|
+
function _ccHintPosition() {
|
|
248
|
+
const mount = document.getElementById(CC_HINT_MOUNT_ID);
|
|
249
|
+
const btn = document.getElementById('cc-toggle-btn');
|
|
250
|
+
if (!mount || !btn) return;
|
|
251
|
+
const rect = btn.getBoundingClientRect();
|
|
252
|
+
mount.style.top = (rect.bottom + 10) + 'px';
|
|
253
|
+
// Right-align the card to the button's right edge.
|
|
254
|
+
mount.style.right = Math.max(8, window.innerWidth - rect.right) + 'px';
|
|
255
|
+
// Point the caret at the button's horizontal centre.
|
|
256
|
+
const arrow = document.getElementById('cc-hint-arrow');
|
|
257
|
+
if (arrow) arrow.style.right = Math.max(12, (rect.width / 2) - 6) + 'px';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let _ccHintResizeBound = false;
|
|
261
|
+
|
|
262
|
+
// Render the coachmark into document.body. Idempotent — bails out (and removes
|
|
263
|
+
// any stale mount) when dismissed, when CC has been used, when the header
|
|
264
|
+
// button isn't mounted yet, or while the drawer is open.
|
|
265
|
+
function renderCcHint() {
|
|
266
|
+
if (_ccHintIsDismissed() || !_ccHintUserIsNew()) {
|
|
267
|
+
_ccHintRemove();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
const btn = document.getElementById('cc-toggle-btn');
|
|
271
|
+
if (!btn) return; // header not assembled yet
|
|
272
|
+
const drawer = document.getElementById('cc-drawer');
|
|
273
|
+
if (drawer && drawer.style.display === 'flex') {
|
|
274
|
+
// User already found CC — don't hover the hint over the open drawer.
|
|
275
|
+
_ccHintRemove();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let mount = document.getElementById(CC_HINT_MOUNT_ID);
|
|
280
|
+
if (!mount) {
|
|
281
|
+
mount = document.createElement('div');
|
|
282
|
+
mount.id = CC_HINT_MOUNT_ID;
|
|
283
|
+
mount.style.cssText = [
|
|
284
|
+
'position:fixed',
|
|
285
|
+
'z-index:330',
|
|
286
|
+
'width:260px',
|
|
287
|
+
'padding:14px 16px',
|
|
288
|
+
'background:var(--surface)',
|
|
289
|
+
'border:1px solid var(--blue)',
|
|
290
|
+
'border-radius:var(--radius-lg)',
|
|
291
|
+
'box-shadow:var(--shadow-md)',
|
|
292
|
+
'color:var(--text)',
|
|
293
|
+
'font-size:var(--text-md)',
|
|
294
|
+
'line-height:1.5',
|
|
295
|
+
].join(';');
|
|
296
|
+
document.body.appendChild(mount);
|
|
297
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: static string literal — no user-controlled data is interpolated
|
|
298
|
+
mount.innerHTML =
|
|
299
|
+
'<div id="cc-hint-arrow" style="position:absolute;top:-7px;width:12px;height:12px;background:var(--surface);border-left:1px solid var(--blue);border-top:1px solid var(--blue);transform:rotate(45deg)"></div>' +
|
|
300
|
+
'<div style="font-weight:700;color:var(--blue);margin-bottom:4px">👋 Start here</div>' +
|
|
301
|
+
'<div style="color:var(--text)">Prompt your <strong>Command Center</strong> here to ask it to dispatch work.</div>' +
|
|
302
|
+
'<div style="display:flex;gap:8px;margin-top:12px">' +
|
|
303
|
+
'<button onclick="openCcFromHint()" class="btn-primary-solid" style="padding:5px 12px;font-size:var(--text-sm)">Open Command Center</button>' +
|
|
304
|
+
'<button onclick="dismissCcHint()" style="padding:5px 12px;font-size:var(--text-sm);background:transparent;color:var(--muted);border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer">Got it</button>' +
|
|
305
|
+
'</div>';
|
|
306
|
+
}
|
|
307
|
+
_ccHintPosition();
|
|
308
|
+
if (!_ccHintResizeBound) {
|
|
309
|
+
_ccHintResizeBound = true;
|
|
310
|
+
window.addEventListener('resize', _ccHintPosition);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
window.MinionsFre = { renderFre, dismissFre, openSettingsToDefaultCli, _freReset, FRE_DISMISS_KEY,
|
|
315
|
+
renderCcHint, dismissCcHint, openCcFromHint, _ccHintReset, _ccHintUserIsNew, CC_HINT_DISMISS_KEY };
|
package/dashboard/js/modal.js
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
// modal.js — Modal and notification badge functions extracted from dashboard.html
|
|
2
2
|
|
|
3
3
|
function closeModal() {
|
|
4
|
+
// P-ce1e5e47 — Esc / X / back-button all route through popModalFrame(),
|
|
5
|
+
// which itself calls history.back(); the popstate listener then pops one
|
|
6
|
+
// breadcrumb and restores the previous modal. When the stack still has
|
|
7
|
+
// depth > 1, the modal stays open with the parent view restored, so we
|
|
8
|
+
// must NOT tear down Q&A / edit / pin state here — only when the modal is
|
|
9
|
+
// actually about to be hidden (depth <= 1).
|
|
10
|
+
var depth = (typeof _modalStackDepth === 'function') ? _modalStackDepth() : 0;
|
|
11
|
+
if (depth > 1) {
|
|
12
|
+
if (typeof popModalFrame === 'function') popModalFrame();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
4
15
|
const modalEl = document.querySelector('#modal .modal');
|
|
5
16
|
if (modalEl) modalEl.classList.remove('modal-wide');
|
|
6
17
|
document.getElementById('modal').classList.remove('open');
|
|
7
|
-
|
|
18
|
+
if (typeof popModalFrame === 'function') popModalFrame();
|
|
19
|
+
else if (typeof clearModalBackStack === 'function') clearModalBackStack();
|
|
8
20
|
// Hide Q&A section (only shown for document modals)
|
|
9
21
|
document.getElementById('modal-qa').style.display = 'none';
|
|
10
22
|
// Remove settings-body marker so the next modal opens with the default
|
|
@@ -132,4 +144,26 @@ function renderArchiveButtons(archives) {
|
|
|
132
144
|
).join(' ');
|
|
133
145
|
}
|
|
134
146
|
|
|
147
|
+
// P-3ed68b1e — Esc key closes the active modal layer. Routes through
|
|
148
|
+
// closeModal() so the depth-aware logic (pop one frame vs full teardown) is
|
|
149
|
+
// the single source of truth. Bubble phase + early-out when the QA review
|
|
150
|
+
// modal is on top so qa.js's own Escape handler isn't shadowed. Inputs and
|
|
151
|
+
// textareas inside the modal are exempt so typing Esc inside Q&A / steering
|
|
152
|
+
// inputs still reaches qa.js / cancel handlers (those listeners use
|
|
153
|
+
// stopPropagation; this guard is belt-and-braces for any future inputs).
|
|
154
|
+
if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
|
|
155
|
+
document.addEventListener('keydown', function(ev) {
|
|
156
|
+
if (ev.key !== 'Escape' && ev.keyCode !== 27) return;
|
|
157
|
+
if (typeof _qaModalState !== 'undefined' && _qaModalState) return;
|
|
158
|
+
var modalEl = document.getElementById('modal');
|
|
159
|
+
if (!modalEl || !modalEl.classList.contains('open')) return;
|
|
160
|
+
var t = ev.target;
|
|
161
|
+
if (t && t.tagName) {
|
|
162
|
+
var tag = String(t.tagName).toUpperCase();
|
|
163
|
+
if (tag === 'INPUT' || tag === 'TEXTAREA' || t.isContentEditable) return;
|
|
164
|
+
}
|
|
165
|
+
closeModal();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
135
169
|
window.MinionsModal = { closeModal, copyModalContent, showNotifBadge, clearNotifBadge, restoreNotifBadges, findCardForFile, renderArchiveButtons };
|
package/dashboard/js/refresh.js
CHANGED
|
@@ -153,7 +153,7 @@ const RENDER_VERSIONS = {
|
|
|
153
153
|
mcpServers: 1,
|
|
154
154
|
harnessDiag: 1,
|
|
155
155
|
schedules: 1,
|
|
156
|
-
watches:
|
|
156
|
+
watches: 3,
|
|
157
157
|
meetings: 1,
|
|
158
158
|
pipelines: 1,
|
|
159
159
|
pinned: 1,
|
|
@@ -612,6 +612,12 @@ function _processStatusUpdate(data, opts) {
|
|
|
612
612
|
if (typeof renderFre === 'function') {
|
|
613
613
|
_safeRender('fre', function() { renderFre(data); });
|
|
614
614
|
}
|
|
615
|
+
// Command Center first-use coachmark — anchored under the CC header button
|
|
616
|
+
// for brand-new operators who have never sent a CC message. Idempotent +
|
|
617
|
+
// cheap; self-suppresses once CC is used or dismissed.
|
|
618
|
+
if (typeof renderCcHint === 'function') {
|
|
619
|
+
_safeRender('ccHint', function() { renderCcHint(); });
|
|
620
|
+
}
|
|
615
621
|
// Notes file (notes.md) is now sourced from /state/notes.md. Same shape
|
|
616
622
|
// as the legacy /api/status notes slice ({ content, updatedAt }); the
|
|
617
623
|
// ETag mtime is parsed out of the response header for updatedAt.
|
|
@@ -71,6 +71,24 @@ function renderAgents(agents) {
|
|
|
71
71
|
restoreDashboardScrollState(grid, scrollState);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Open the right-side agent detail panel.
|
|
76
|
+
*
|
|
77
|
+
* P-30b6cf8a — Detail-panel exception. This panel is intentionally NOT a
|
|
78
|
+
* modal-stack frame; it is a peer of the stack. Routing comes through
|
|
79
|
+
* openArtifact('agent', id) in dashboard/js/render-utils.js, which:
|
|
80
|
+
* 1. Calls resetModalStack() + _physicallyCloseModal() so the modal-stack
|
|
81
|
+
* identity goes back to empty (any prior modal is dismissed).
|
|
82
|
+
* 2. Calls openAgentDetail(id), which opens this slide-in panel.
|
|
83
|
+
*
|
|
84
|
+
* Reverse direction is symmetric: clicking a WI/PR/plan chip from inside
|
|
85
|
+
* the panel calls openArtifact('<type>', id), which closes this panel and
|
|
86
|
+
* starts a fresh modal stack. This is the one documented exception to
|
|
87
|
+
* in-stack chip behaviour — do NOT refactor it into a normal modal frame.
|
|
88
|
+
* The panel UI (tabs, live-output stream, charter editor) does not fit
|
|
89
|
+
* the single-shell modal contract; trying to force it into pushModalFrame
|
|
90
|
+
* would lose the tabbed layout and the persistent live-stream socket.
|
|
91
|
+
*/
|
|
74
92
|
async function openAgentDetail(id) {
|
|
75
93
|
const agent = agentData.find(a => a.id === id);
|
|
76
94
|
if (!agent) return;
|
|
@@ -251,8 +251,33 @@ async function kbOpenItem(category, file) {
|
|
|
251
251
|
const display = content.replace(/^---[\s\S]*?---\n*/m, '');
|
|
252
252
|
document.getElementById('modal-title').textContent = file;
|
|
253
253
|
const modalBody = document.getElementById('modal-body');
|
|
254
|
-
//
|
|
255
|
-
|
|
254
|
+
// P-34fa5d79 — KB modal renders "From note: …" and "From WI: …" chips
|
|
255
|
+
// when the entry was promoted via /api/inbox/promote-kb (which stamps
|
|
256
|
+
// source_note + source_wi). Parse the frontmatter block off the raw
|
|
257
|
+
// fetched content (we already strip it for `display`).
|
|
258
|
+
var _fmChipsHtml = '';
|
|
259
|
+
var _fmMatch = String(content || '').match(/^---\n([\s\S]*?)\n---/);
|
|
260
|
+
if (_fmMatch) {
|
|
261
|
+
var _fm = {};
|
|
262
|
+
_fmMatch[1].split('\n').forEach(function(line) {
|
|
263
|
+
var lm = line.match(/^([\w-]+):\s*(.*)$/);
|
|
264
|
+
if (lm) _fm[lm[1]] = lm[2].trim();
|
|
265
|
+
});
|
|
266
|
+
var _chips = [];
|
|
267
|
+
if (_fm.source_note) {
|
|
268
|
+
_chips.push('<span style="color:var(--muted);font-size:var(--text-sm);text-transform:uppercase;letter-spacing:0.5px;margin-right:4px">From note</span>' +
|
|
269
|
+
renderArtifactLink({ type: 'note', id: _fm.source_note, label: _fm.source_note.replace(/\.md$/, '').slice(0, 30), title: 'Source note: ' + _fm.source_note }));
|
|
270
|
+
}
|
|
271
|
+
if (_fm.source_wi) {
|
|
272
|
+
_chips.push('<span style="color:var(--muted);font-size:var(--text-sm);text-transform:uppercase;letter-spacing:0.5px;margin-right:4px">From WI</span>' +
|
|
273
|
+
renderArtifactLink({ type: 'wi', id: _fm.source_wi, label: _fm.source_wi, title: 'Source work item: ' + _fm.source_wi }));
|
|
274
|
+
}
|
|
275
|
+
if (_chips.length > 0) {
|
|
276
|
+
_fmChipsHtml = '<div style="margin-bottom:10px;padding:6px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm);display:flex;flex-wrap:wrap;gap:12px;align-items:center">' + _chips.join('') + '</div>';
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes all user-controlled fields before assembling HTML (see dashboard/js/utils.js); _fmChipsHtml is built from renderArtifactLink() chips which escape id+label via escapeHtml
|
|
280
|
+
modalBody.innerHTML = _fmChipsHtml + renderMd(display);
|
|
256
281
|
_modalDocContext = { title: file, content: display, selection: '' };
|
|
257
282
|
_modalFilePath = 'knowledge/' + category + '/' + file; showModalQa();
|
|
258
283
|
// Clear notification badge when opening this document
|
|
@@ -196,6 +196,30 @@ function _renderMeetingDetail(m) {
|
|
|
196
196
|
'<span style="font-size:var(--text-sm);color:var(--muted)">' + escHtml(m.createdAt?.slice(0, 16).replace('T', ' ') || '') + '</span>' +
|
|
197
197
|
'</div>';
|
|
198
198
|
|
|
199
|
+
// P-30b6cf8a — Linked-artifact chip row. Renders the meeting.linkedPlan
|
|
200
|
+
// (derived by _findLinkedPlan from conclusion text / title-slug match)
|
|
201
|
+
// and the participants list as in-stack renderArtifactLink chips.
|
|
202
|
+
// Plan chip routes through openArtifact -> planView (in-stack modal
|
|
203
|
+
// push, browser Back returns here). Agent chips route through the
|
|
204
|
+
// detail-panel exception in openArtifact (resetModalStack + open the
|
|
205
|
+
// slide-in panel); see render-utils.js openArtifact comment.
|
|
206
|
+
const _linkedPlanForChip = _findLinkedPlan(m);
|
|
207
|
+
const _hasChipRenderer = typeof renderArtifactLink === 'function';
|
|
208
|
+
if (_hasChipRenderer && (_linkedPlanForChip || (m.participants || []).length > 0)) {
|
|
209
|
+
let chipRow = '<div style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;font-size:var(--text-sm)">';
|
|
210
|
+
if (_linkedPlanForChip) {
|
|
211
|
+
chipRow += '<span style="color:var(--muted)">Linked plan:</span> ' +
|
|
212
|
+
renderArtifactLink({ type: 'plan', id: _linkedPlanForChip.file, label: _linkedPlanForChip.file, title: _linkedPlanForChip.summary || _linkedPlanForChip.file });
|
|
213
|
+
}
|
|
214
|
+
if ((m.participants || []).length > 0) {
|
|
215
|
+
chipRow += (_linkedPlanForChip ? ' <span style="color:var(--border)">·</span> ' : '') +
|
|
216
|
+
'<span style="color:var(--muted)">Participants:</span> ' +
|
|
217
|
+
(m.participants || []).map(a => renderArtifactLink({ type: 'agent', id: a, label: a, title: 'Open ' + a + ' detail panel' })).join(' ');
|
|
218
|
+
}
|
|
219
|
+
chipRow += '</div>';
|
|
220
|
+
html += chipRow;
|
|
221
|
+
}
|
|
222
|
+
|
|
199
223
|
// Agenda — render markdown-like formatting
|
|
200
224
|
html += '<div style="background:var(--surface2);padding:8px 12px;border-radius:6px;font-size:var(--text-md);white-space:pre-wrap;line-height:1.6">' +
|
|
201
225
|
'<strong>Agenda:</strong>\n' + renderMd(m.agenda || '') + '</div>';
|
|
@@ -203,7 +227,13 @@ function _renderMeetingDetail(m) {
|
|
|
203
227
|
// Per-agent panels
|
|
204
228
|
for (const agent of (m.participants || [])) {
|
|
205
229
|
html += '<div style="border:1px solid var(--border);border-radius:6px;overflow:hidden">';
|
|
206
|
-
|
|
230
|
+
// P-30b6cf8a — agent header is a chip so it routes through the
|
|
231
|
+
// detail-panel exception in openArtifact (resetModalStack + open
|
|
232
|
+
// slide-in panel).
|
|
233
|
+
const _agentChip = _hasChipRenderer
|
|
234
|
+
? renderArtifactLink({ type: 'agent', id: agent, label: agent, title: 'Open ' + agent + ' detail panel' })
|
|
235
|
+
: escHtml(agent);
|
|
236
|
+
html += '<div style="background:var(--surface2);padding:6px 12px;font-weight:600;font-size:var(--text-md)">' + _agentChip + '</div>';
|
|
207
237
|
|
|
208
238
|
// Findings
|
|
209
239
|
if (m.findings?.[agent]) {
|
|
@@ -322,8 +352,11 @@ function openMeetingDetail(id) {
|
|
|
322
352
|
if (d.meeting && _meetingPollId === id) {
|
|
323
353
|
const hash = JSON.stringify(d.meeting);
|
|
324
354
|
if (hash === _lastMeetingHash) return; // no change — skip re-render
|
|
325
|
-
|
|
326
|
-
|
|
355
|
+
// P-ce1e5e47 — withTopFrame skips the re-render when the user
|
|
356
|
+
// has stacked another modal on top of this meeting view.
|
|
357
|
+
const apply = function() { _lastMeetingHash = hash; _renderMeetingDetail(d.meeting); };
|
|
358
|
+
if (typeof withTopFrame === 'function') withTopFrame('meeting', id, apply);
|
|
359
|
+
else apply();
|
|
327
360
|
}
|
|
328
361
|
})
|
|
329
362
|
.catch(function() {});
|
|
@@ -98,29 +98,34 @@ function _getPipelineDisplayTitle(pipeline) {
|
|
|
98
98
|
|
|
99
99
|
/**
|
|
100
100
|
* Render clickable artifact links for a pipeline stage.
|
|
101
|
-
*
|
|
101
|
+
* Navigation chips (workItems / meetings / plans / prds) route through
|
|
102
|
+
* renderArtifactLink → openArtifact() so the modal stack push and URL hash
|
|
103
|
+
* come for free (P-e265cd31). PRs / notes / subStages stay inline: PRs and
|
|
104
|
+
* notes page-switch instead of opening a detail modal, and subStages are
|
|
105
|
+
* non-clickable — none has an openArtifact path, so the "View PR" / "View
|
|
106
|
+
* Note" pills render identically to today.
|
|
102
107
|
* @param {Object} artifacts - artifact map from stage run
|
|
103
|
-
* @param {string} [pipelineId] -
|
|
108
|
+
* @param {string} [pipelineId] - retained for back-compat; unused now that
|
|
109
|
+
* openArtifact captures the current visible modal automatically.
|
|
104
110
|
*/
|
|
105
|
-
function _renderArtifactLinks(artifacts, pipelineId) {
|
|
111
|
+
function _renderArtifactLinks(artifacts /* , pipelineId — unused since P-e265cd31 */) {
|
|
106
112
|
if (!artifacts) return '';
|
|
107
113
|
var links = [];
|
|
108
114
|
var linkStyle = 'display:inline-flex;align-items:center;gap:2px;padding:1px 6px;border-radius:10px;font-size:var(--text-sm);cursor:pointer;text-decoration:none;color:var(--blue);background:color-mix(in srgb, var(--blue) 10%, transparent);border:1px solid color-mix(in srgb, var(--blue) 20%, transparent)';
|
|
109
115
|
|
|
110
|
-
// Pushes current pipeline modal onto back stack so detail modals can navigate back
|
|
111
|
-
var backFn = pipelineId ? "pushModalBack(function(){openPipelineDetail('" + escHtml(pipelineId) + "')});" : '';
|
|
112
|
-
|
|
113
116
|
(artifacts.workItems || []).forEach(function(id) {
|
|
114
|
-
links.push(
|
|
117
|
+
links.push(renderArtifactLink({ type: 'wi', id: id, label: id, title: 'Open work item ' + id }));
|
|
115
118
|
});
|
|
116
119
|
(artifacts.meetings || []).forEach(function(id) {
|
|
117
|
-
links.push(
|
|
120
|
+
links.push(renderArtifactLink({ type: 'meeting', id: id, label: id, title: 'Open meeting ' + id }));
|
|
118
121
|
});
|
|
119
122
|
(artifacts.plans || []).forEach(function(name) {
|
|
120
|
-
|
|
123
|
+
var label = String(name).replace(/\.md$/, '').slice(0, 30);
|
|
124
|
+
links.push(renderArtifactLink({ type: 'plan', id: name, label: label, title: 'Plan: ' + name }));
|
|
121
125
|
});
|
|
122
126
|
(artifacts.prds || []).forEach(function(name) {
|
|
123
|
-
|
|
127
|
+
var label = String(name).replace(/\.json$/, '').slice(0, 30);
|
|
128
|
+
links.push(renderArtifactLink({ type: 'prd', id: name, label: label, title: 'PRD: ' + name }));
|
|
124
129
|
});
|
|
125
130
|
(artifacts.prs || []).forEach(function(id) {
|
|
126
131
|
links.push('<span style="' + linkStyle + '" onclick="event.stopPropagation();closeModal();switchPage(\'prs\')" title="Pull request ' + escHtml(id) + '">🔀 PR-' + escHtml(id) + '</span>');
|
|
@@ -697,11 +697,73 @@ function _renderPlanModal(normalizedFile, raw, lastMod) {
|
|
|
697
697
|
modalActions += '<button class="pr-pager-btn" style="' + bs + ';color:var(--red)" onclick="planDelete(\'' + escapeHtml(normalizedFile) + '\')">Delete</button>';
|
|
698
698
|
}
|
|
699
699
|
|
|
700
|
+
// P-79b47b0c — surface linked artifacts (PRD + verify-guide) as in-stack
|
|
701
|
+
// renderArtifactLink chips alongside the plan modal action buttons.
|
|
702
|
+
// P-30b6cf8a — also surface the source-meeting chip when the PRD JSON
|
|
703
|
+
// carries a `meetingId` field or the .md plan body contains the
|
|
704
|
+
// `**Source Meeting:** MTG-…` header line (the dashboard convention
|
|
705
|
+
// written by POST /api/plans/create when a plan is created from a
|
|
706
|
+
// meeting). Chip routes through openArtifact → openMeetingDetail for
|
|
707
|
+
// in-stack push (Back returns here).
|
|
708
|
+
let linkedChips = '';
|
|
709
|
+
if (typeof renderArtifactLink === 'function') {
|
|
710
|
+
if (prdFile && prdFile !== normalizedFile) {
|
|
711
|
+
linkedChips += renderArtifactLink({ type: 'prd', id: prdFile, label: 'PRD', title: prdFile }) + ' ';
|
|
712
|
+
}
|
|
713
|
+
let meetingChipId = '';
|
|
714
|
+
if (normalizedFile.endsWith('.json')) {
|
|
715
|
+
try {
|
|
716
|
+
const _parsedForMtg = JSON.parse(raw);
|
|
717
|
+
if (_parsedForMtg && typeof _parsedForMtg.meetingId === 'string' && /^MTG-[\w-]+$/.test(_parsedForMtg.meetingId)) {
|
|
718
|
+
meetingChipId = _parsedForMtg.meetingId;
|
|
719
|
+
}
|
|
720
|
+
} catch { /* JSON parse already surfaced upstream */ }
|
|
721
|
+
} else {
|
|
722
|
+
const _mtgMatch = (raw || '').match(/\*\*Source Meeting:\*\*\s+(MTG-[\w-]+)/);
|
|
723
|
+
if (_mtgMatch) meetingChipId = _mtgMatch[1];
|
|
724
|
+
}
|
|
725
|
+
if (meetingChipId) {
|
|
726
|
+
linkedChips += renderArtifactLink({ type: 'meeting', id: meetingChipId, label: meetingChipId, title: 'Source meeting: ' + meetingChipId }) + ' ';
|
|
727
|
+
}
|
|
728
|
+
const guidesForChip = window._lastVerifyGuides || [];
|
|
729
|
+
const guideForChip = guidesForChip.find(g => g && g.planFile === (prdFile || normalizedFile));
|
|
730
|
+
if (guideForChip && guideForChip.file) {
|
|
731
|
+
// Verify guides live at prd/guides/verify-<slug>.md and are fetched via
|
|
732
|
+
// /api/plans/<file>, so a plain {type:'plan'} chip resolves correctly
|
|
733
|
+
// through openArtifact -> planView. openVerifyGuide() is the legacy
|
|
734
|
+
// helper that does the same thing — we keep the modal-stack semantics
|
|
735
|
+
// by routing through openArtifact instead.
|
|
736
|
+
linkedChips += renderArtifactLink({ type: 'plan', id: guideForChip.file, label: 'Verify Guide', icon: '📖', title: 'Manual testing guide (openVerifyGuide)' }) + ' ';
|
|
737
|
+
}
|
|
738
|
+
// P-e6093f70 — cross-repo plan ↔ plan sibling chips. When the plan
|
|
739
|
+
// fans out across ≥2 projects (lifecycle.checkPlanCompletion creates
|
|
740
|
+
// one verify WI per touched project — see CLAUDE.md §"Cross-repo
|
|
741
|
+
// plans" invariant 5), surface a clickable WI chip per sibling so
|
|
742
|
+
// operators can hop between the per-project verify WIs without
|
|
743
|
+
// leaving the modal stack. The chip label is the project slug so each
|
|
744
|
+
// sibling is identifiable at a glance (mirrors _renderVerifyBadge's
|
|
745
|
+
// projectLabel handling at line ~1042). Gate matches the existing
|
|
746
|
+
// per-project verify-badge fanout gate at line ~689 so single-project
|
|
747
|
+
// plans are unaffected.
|
|
748
|
+
if (modalVerifyWis.length >= 2) {
|
|
749
|
+
for (const v of modalVerifyWis) {
|
|
750
|
+
const projLabel = v.project || 'verify';
|
|
751
|
+
linkedChips += renderArtifactLink({
|
|
752
|
+
type: 'wi',
|
|
753
|
+
id: v.id,
|
|
754
|
+
label: projLabel,
|
|
755
|
+
title: 'Sibling verify WI for ' + projLabel + ' (cross-repo plan fanout)',
|
|
756
|
+
}) + ' ';
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
700
761
|
const lastModLabel = lastMod ? '<div style="font-size:var(--text-sm);color:var(--muted);font-weight:400;margin-top:2px">Last updated: ' + formatLocalDateTime(lastMod) + '</div>' : '';
|
|
762
|
+
const linkedChipsRow = linkedChips ? '<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px">' + linkedChips + '</div>' : '';
|
|
701
763
|
const actionBtns = '<div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px">' + modalActions + '</div>';
|
|
702
764
|
|
|
703
|
-
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() (fields: plan title, version label, action target files)
|
|
704
|
-
document.getElementById('modal-title').innerHTML = escapeHtml(title) + (versionLabel ? ' <span style="font-size:var(--text-base);font-weight:700;padding:1px 6px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue)">' + escapeHtml(versionLabel) + '</span>' : '') + lastModLabel + actionBtns;
|
|
765
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() (fields: plan title, version label, action target files); linkedChipsRow output is renderArtifactLink chips (escapes internally) or empty
|
|
766
|
+
document.getElementById('modal-title').innerHTML = escapeHtml(title) + (versionLabel ? ' <span style="font-size:var(--text-base);font-weight:700;padding:1px 6px;border-radius:3px;background:rgba(56,139,253,0.15);color:var(--blue)">' + escapeHtml(versionLabel) + '</span>' : '') + lastModLabel + linkedChipsRow + actionBtns;
|
|
705
767
|
const modalBody = document.getElementById('modal-body');
|
|
706
768
|
const scrollTop = modalBody.scrollTop;
|
|
707
769
|
if (normalizedFile.endsWith('.json')) {
|
|
@@ -748,7 +810,15 @@ async function planView(file) {
|
|
|
748
810
|
}
|
|
749
811
|
fetch('/api/plans/' + encodeURIComponent(normalizedFile))
|
|
750
812
|
.then(function(r) { return r.text().then(function(raw) { return { raw: raw, lastMod: r.headers.get('Last-Modified') }; }); })
|
|
751
|
-
.then(function(d) {
|
|
813
|
+
.then(function(d) {
|
|
814
|
+
// P-ce1e5e47 — withTopFrame guards a stacked modal: if the user
|
|
815
|
+
// navigated to a deeper modal on top of this plan view, skip the
|
|
816
|
+
// re-render so we don't clobber the visible top frame.
|
|
817
|
+
if (_planPollFile !== normalizedFile || d.raw === _planPollLastRaw) return;
|
|
818
|
+
var apply = function() { _planPollLastRaw = d.raw; _renderPlanModal(normalizedFile, d.raw, d.lastMod); };
|
|
819
|
+
if (typeof withTopFrame === 'function') withTopFrame('plan', normalizedFile, apply);
|
|
820
|
+
else apply();
|
|
821
|
+
})
|
|
752
822
|
.catch(function() {});
|
|
753
823
|
}, 3000);
|
|
754
824
|
} catch (e) { console.error(e); }
|
|
@@ -144,9 +144,16 @@ function renderPrd(prd, prog) {
|
|
|
144
144
|
|
|
145
145
|
function _renderPrLink(pr, opts) {
|
|
146
146
|
var size = (opts && opts.size) || '10px';
|
|
147
|
-
var statusColor = pr.status === 'merged' ? 'var(--green)' : pr.status === 'abandoned' ? 'var(--red)' : 'var(--blue)';
|
|
148
147
|
var statusIcon = pr.status === 'merged' ? '✓' : pr.status === 'abandoned' ? '✗' : '○';
|
|
149
|
-
|
|
148
|
+
// P-79b47b0c — render as an in-stack modal chip via renderArtifactLink
|
|
149
|
+
// (was a raw <a target="_blank">). The status icon stays as a non-link
|
|
150
|
+
// prefix so the chip body remains the canonical id, matching the WI/PRD
|
|
151
|
+
// chip pattern. Click is gated by openArtifact -> openPrDetail.
|
|
152
|
+
var chip = (typeof renderArtifactLink === 'function')
|
|
153
|
+
? renderArtifactLink({ type: 'pr', id: pr.id, label: pr.id, title: (pr.title || '') + ' (' + (pr.status || 'active') + ')' })
|
|
154
|
+
: '<code>' + escHtml(pr.id) + '</code>';
|
|
155
|
+
return '<span style="font-size:' + size + ';margin-left:4px;display:inline-flex;align-items:center;gap:3px">' +
|
|
156
|
+
'<span title="' + escHtml(pr.status || 'active') + '">' + statusIcon + '</span>' + chip + '</span>';
|
|
150
157
|
}
|
|
151
158
|
|
|
152
159
|
function _renderPrdWorkItemIdBadge(workItemId, prdItemId, size, padding) {
|
|
@@ -288,9 +295,19 @@ function renderPrdProgress(prog) {
|
|
|
288
295
|
? '<span onclick="event.stopPropagation();prdItemReopen(\'' + escHtml(i.source || '') + '\',\'' + escHtml(i.id) + '\')" style="color:var(--blue);cursor:pointer;font-size:var(--text-xs);padding:1px 5px;background:rgba(56,139,253,0.1);border:1px solid rgba(56,139,253,0.3);border-radius:3px" title="Re-open: set to updated so engine re-dispatches on existing branch">re-open</span>'
|
|
289
296
|
: '';
|
|
290
297
|
|
|
298
|
+
// P-79b47b0c — replace the bare prd-item-id span with a renderArtifactLink
|
|
299
|
+
// chip pointing at the backing WI (when one exists). The chip's onclick
|
|
300
|
+
// calls event.stopPropagation, so the parent row's prdItemEdit() still
|
|
301
|
+
// fires only on row-body clicks; clicking the chip opens the WI detail
|
|
302
|
+
// modal in-stack. When there's no backing WI, fall back to the legacy
|
|
303
|
+
// bare-id span (clicking the row still opens prdItemEdit).
|
|
304
|
+
const idChip = (wi && typeof renderArtifactLink === 'function')
|
|
305
|
+
? renderArtifactLink({ type: 'wi', id: wi.id, label: i.id, title: i.name || wi.title || i.id })
|
|
306
|
+
: '<span class="prd-item-id">' + escHtml(i.id) + '</span>';
|
|
307
|
+
|
|
291
308
|
return '<div class="prd-item-row st-' + (i.status || 'missing') + '" style="flex-wrap:wrap;cursor:pointer" onclick="if(shouldIgnoreSelectionClick(event))return;prdItemEdit(\'' + src + '\',\'' + iid + '\')">' +
|
|
292
309
|
statusBadge(i.status, i.id) +
|
|
293
|
-
|
|
310
|
+
idChip +
|
|
294
311
|
'<span class="prd-item-name" title="' + escHtml(i.name) + '">' + escHtml(i.name) + '</span>' +
|
|
295
312
|
wiLabel +
|
|
296
313
|
agentLabel +
|
|
@@ -600,9 +617,13 @@ function renderPrdProgress(prog) {
|
|
|
600
617
|
html += '<div style="font-size:var(--text-sm);font-weight:600;color:var(--blue);margin-bottom:4px">E2E Aggregate PRs</div>';
|
|
601
618
|
html += prs.map(pr => {
|
|
602
619
|
const statusColor = pr.status === 'active' ? 'var(--green)' : pr.status === 'merged' ? 'var(--purple)' : 'var(--muted)';
|
|
620
|
+
// P-79b47b0c — render PR id as in-stack chip (was raw <a target="_blank">).
|
|
621
|
+
const prChip = (typeof renderArtifactLink === 'function')
|
|
622
|
+
? renderArtifactLink({ type: 'pr', id: pr.id, label: pr.id, title: pr.title || pr.id })
|
|
623
|
+
: '<code>' + escHtml(pr.id) + '</code>';
|
|
603
624
|
return '<div style="display:flex;align-items:center;gap:6px;padding:2px 0;font-size:var(--text-base)">' +
|
|
604
625
|
'<span style="color:' + statusColor + ';font-size:var(--text-xs);font-weight:600;padding:1px 4px;border:1px solid;border-radius:3px">' + escHtml(pr.status || 'active') + '</span>' +
|
|
605
|
-
|
|
626
|
+
prChip +
|
|
606
627
|
'<span style="color:var(--text);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + escHtml(pr.title || '') + '</span>' +
|
|
607
628
|
'<span style="color:var(--muted);font-size:var(--text-xs)">' + escHtml(pr._project || '') + '</span>' +
|
|
608
629
|
'</div>';
|
|
@@ -791,7 +812,10 @@ async function prdItemEdit(source, itemId) {
|
|
|
791
812
|
const completedAt = wi?.completedAt || completedEntry?.completed_at || '';
|
|
792
813
|
const summary = completedEntry?.resultSummary || '';
|
|
793
814
|
const prLinks = (item.prs || []).map(function(pr) {
|
|
794
|
-
|
|
815
|
+
// P-79b47b0c — render as in-stack chip (was raw <a target="_blank">).
|
|
816
|
+
return (typeof renderArtifactLink === 'function')
|
|
817
|
+
? renderArtifactLink({ type: 'pr', id: pr.id, label: pr.id, title: pr.title || pr.id })
|
|
818
|
+
: '<code>' + escHtml(pr.id) + '</code>';
|
|
795
819
|
}).join(', ');
|
|
796
820
|
|
|
797
821
|
const statusColor = isDone ? 'var(--green)' : isFailed ? 'var(--red)' : 'var(--blue)';
|