@yemi33/minions 0.1.2195 → 0.1.2197
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/js/modal-qa.js +21 -3
- package/dashboard/js/modal.js +35 -1
- package/dashboard/js/refresh.js +1 -1
- package/dashboard/js/render-agents.js +18 -0
- package/dashboard/js/render-kb.js +27 -2
- package/dashboard/js/render-meetings.js +36 -3
- package/dashboard/js/render-pipelines.js +15 -10
- package/dashboard/js/render-plans.js +73 -3
- package/dashboard/js/render-prd.js +29 -5
- package/dashboard/js/render-prs.js +174 -3
- package/dashboard/js/render-schedules.js +12 -0
- package/dashboard/js/render-utils.js +151 -1
- package/dashboard/js/render-watches.js +61 -7
- package/dashboard/js/render-work-items.js +161 -20
- package/dashboard/js/state.js +110 -1
- package/dashboard/js/utils.js +246 -14
- package/dashboard/layout.html +2 -0
- package/dashboard/slim/body.html +23 -13
- package/dashboard/slim/js/knowledge.js +576 -0
- package/dashboard/slim/js/members.js +43 -0
- package/dashboard/slim/js/modals-tiles.js +6 -4
- package/dashboard/slim/js/pinned.js +7 -19
- package/dashboard/slim/js/status.js +17 -21
- package/dashboard/slim/styles.css +94 -36
- package/dashboard/styles.css +34 -0
- package/dashboard-build.js +1 -1
- package/dashboard.js +50 -2
- package/engine/dispatch.js +39 -4
- package/engine/lifecycle.js +27 -7
- package/engine/pipeline.js +10 -0
- package/engine/queries.js +81 -0
- package/engine/scheduler.js +19 -1
- package/engine/shared.js +35 -13
- package/engine.js +4 -6
- package/package.json +1 -1
package/dashboard/js/modal-qa.js
CHANGED
|
@@ -335,6 +335,19 @@ function _qaBuildRawErrorHtml(err) {
|
|
|
335
335
|
'</details>';
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
+
function _qaDocStreamErrorMessage(evt) {
|
|
339
|
+
if (!evt) return 'Failed';
|
|
340
|
+
if (typeof evt.error === 'string' && evt.error) return evt.error;
|
|
341
|
+
if (evt.message) return String(evt.message);
|
|
342
|
+
if (evt.errorMessage) return String(evt.errorMessage);
|
|
343
|
+
if (evt.error && typeof evt.error === 'object') {
|
|
344
|
+
if (evt.error.errorMessage) return String(evt.error.errorMessage);
|
|
345
|
+
if (evt.error.message) return String(evt.error.message);
|
|
346
|
+
if (evt.error.stderr) return String(evt.error.stderr);
|
|
347
|
+
}
|
|
348
|
+
return 'Failed';
|
|
349
|
+
}
|
|
350
|
+
|
|
338
351
|
function _qaBuildAssistantHtml(text, opts) {
|
|
339
352
|
const body = opts?.isError ? escHtml(text) : renderMd(text);
|
|
340
353
|
const style = opts?.isError
|
|
@@ -713,6 +726,7 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
713
726
|
const decoder = new TextDecoder();
|
|
714
727
|
let buf = '';
|
|
715
728
|
let terminalEventSeen = false;
|
|
729
|
+
let pendingStreamErrorEvent = null;
|
|
716
730
|
|
|
717
731
|
async function _qaHandleStreamEvent(evt) {
|
|
718
732
|
if (!evt || !evt.type) return;
|
|
@@ -842,8 +856,8 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
842
856
|
return;
|
|
843
857
|
}
|
|
844
858
|
if (evt.type === 'error') {
|
|
845
|
-
|
|
846
|
-
|
|
859
|
+
pendingStreamErrorEvent = evt;
|
|
860
|
+
return;
|
|
847
861
|
}
|
|
848
862
|
}
|
|
849
863
|
|
|
@@ -867,7 +881,11 @@ async function _processQaMessage(message, selection, opts) {
|
|
|
867
881
|
await _qaHandleStreamEvent(JSON.parse(line.slice(6)));
|
|
868
882
|
}
|
|
869
883
|
}
|
|
870
|
-
if (!terminalEventSeen)
|
|
884
|
+
if (!terminalEventSeen) {
|
|
885
|
+
throw new Error(pendingStreamErrorEvent
|
|
886
|
+
? _qaDocStreamErrorMessage(pendingStreamErrorEvent)
|
|
887
|
+
: 'The response stream ended before completion.');
|
|
888
|
+
}
|
|
871
889
|
} catch (e) {
|
|
872
890
|
clearInterval(qaTimer);
|
|
873
891
|
_clearQaStreamWatchdog();
|
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
|
@@ -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)';
|