@yemi33/minions 0.1.2195 → 0.1.2196
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/js/modal.js +35 -1
- package/dashboard/js/refresh.js +1 -1
- package/dashboard/js/render-agents.js +18 -0
- package/dashboard/js/render-kb.js +27 -2
- package/dashboard/js/render-meetings.js +36 -3
- package/dashboard/js/render-pipelines.js +15 -10
- package/dashboard/js/render-plans.js +73 -3
- package/dashboard/js/render-prd.js +29 -5
- package/dashboard/js/render-prs.js +174 -3
- package/dashboard/js/render-schedules.js +12 -0
- package/dashboard/js/render-utils.js +151 -1
- package/dashboard/js/render-watches.js +61 -7
- package/dashboard/js/render-work-items.js +161 -20
- package/dashboard/js/state.js +110 -1
- package/dashboard/js/utils.js +246 -14
- package/dashboard/layout.html +2 -0
- package/dashboard/slim/body.html +23 -13
- package/dashboard/slim/js/knowledge.js +576 -0
- package/dashboard/slim/js/members.js +43 -0
- package/dashboard/slim/js/modals-tiles.js +6 -4
- package/dashboard/slim/js/pinned.js +7 -19
- package/dashboard/slim/js/status.js +17 -21
- package/dashboard/slim/styles.css +94 -36
- package/dashboard/styles.css +34 -0
- package/dashboard-build.js +1 -1
- package/dashboard.js +50 -2
- package/engine/lifecycle.js +6 -0
- package/engine/pipeline.js +10 -0
- package/engine/queries.js +81 -0
- package/engine/scheduler.js +19 -1
- package/package.json +1 -1
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)';
|
|
@@ -204,7 +204,8 @@ function prRow(pr) {
|
|
|
204
204
|
'<td><span class="pr-badge ' + statusClass + '" title="' + escapeHtml(statusLabel) + '">' + escapeHtml(statusLabel) + '</span></td>' +
|
|
205
205
|
'<td>' + (observeBtn || '<span style="color:var(--muted);font-size:var(--text-base)">—</span>') + '</td>' +
|
|
206
206
|
'<td><span class="pr-date" title="' + escapeHtml(createdLabel) + '">' + escapeHtml(createdLabel) + '</span></td>' +
|
|
207
|
-
'<td><button class="
|
|
207
|
+
'<td><button class="pr-info-pill" data-pr-id="' + escapeHtml(String(prId)) + '" onclick="event.stopPropagation();openPrDetail(this.dataset.prId)" title="Open PR detail (in-stack modal)">↗</button>' +
|
|
208
|
+
'<button class="btn-destructive" style="padding:1px 5px;margin-left:4px" data-pr-id="' + escapeHtml(String(prId)) + '" onclick="event.stopPropagation();unlinkPr(this.dataset.prId)" title="Remove from tracking">x</button></td>' +
|
|
208
209
|
'</tr>';
|
|
209
210
|
}
|
|
210
211
|
|
|
@@ -224,7 +225,7 @@ const PRS_COLGROUP =
|
|
|
224
225
|
'<col style="width:110px">' + // Status
|
|
225
226
|
'<col style="width:100px">' + // Observe
|
|
226
227
|
'<col style="width:130px">' + // Created
|
|
227
|
-
'<col style="width:
|
|
228
|
+
'<col style="width:80px">' + // Actions (info-pill + remove)
|
|
228
229
|
'</colgroup>';
|
|
229
230
|
|
|
230
231
|
function prTableHtml(rows) {
|
|
@@ -300,6 +301,176 @@ function openAllPrs() {
|
|
|
300
301
|
document.getElementById('modal').classList.add('open');
|
|
301
302
|
}
|
|
302
303
|
|
|
304
|
+
// P-79b47b0c — in-stack PR detail modal. Routed from openArtifact('pr', id)
|
|
305
|
+
// in render-utils.js and from the PR-row info-pill in prRow(). Read-only:
|
|
306
|
+
// renders title, status/review/build badges, reviewers, linked-WI chips
|
|
307
|
+
// (joined via item._pr === pr.id from queries enrichment), description, and
|
|
308
|
+
// an "Open on GitHub" secondary button. No POSTs from this modal.
|
|
309
|
+
function openPrDetail(prId) {
|
|
310
|
+
if (window.getSelection && window.getSelection().toString().length > 0) return;
|
|
311
|
+
if (!prId) return;
|
|
312
|
+
// Render an immediate skeleton from the in-memory PR cache (allPrs) so the
|
|
313
|
+
// modal feels instant. Then hit GET /api/prs/<id> for the canonical record
|
|
314
|
+
// and re-render via withTopFrame() to respect modal-stack semantics.
|
|
315
|
+
const cached = (allPrs || []).find(p => p && p.id === prId) ||
|
|
316
|
+
((window._lastPrs) || []).find(p => p && p.id === prId);
|
|
317
|
+
document.getElementById('modal-title').textContent = (cached && (cached.title || cached.id)) || prId;
|
|
318
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: _renderPrDetail() escapes all user fields (title, description, agent, reviewers, branch) via escapeHtml() / renderMd() / renderArtifactLink() before assembly
|
|
319
|
+
document.getElementById('modal-body').innerHTML = _renderPrDetail(cached || { id: prId, _loading: true });
|
|
320
|
+
document.getElementById('modal-body').style.fontFamily = "'Segoe UI', system-ui, sans-serif";
|
|
321
|
+
document.getElementById('modal-body').style.whiteSpace = 'normal';
|
|
322
|
+
document.getElementById('modal').classList.add('open');
|
|
323
|
+
|
|
324
|
+
fetch('/api/prs/' + encodeURIComponent(prId))
|
|
325
|
+
.then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
|
326
|
+
.then(function(data) {
|
|
327
|
+
const pr = data && data.pr;
|
|
328
|
+
if (!pr) return;
|
|
329
|
+
const apply = function() {
|
|
330
|
+
document.getElementById('modal-title').textContent = pr.title || pr.id || prId;
|
|
331
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: _renderPrDetail() escapes all user fields (title, description, agent, reviewers, branch) via escapeHtml() / renderMd() / renderArtifactLink() before assembly
|
|
332
|
+
document.getElementById('modal-body').innerHTML = _renderPrDetail(pr);
|
|
333
|
+
};
|
|
334
|
+
if (typeof withTopFrame === 'function') withTopFrame('pr', prId, apply);
|
|
335
|
+
else apply();
|
|
336
|
+
})
|
|
337
|
+
.catch(function() {
|
|
338
|
+
const body = document.getElementById('modal-body');
|
|
339
|
+
if (body) {
|
|
340
|
+
body.innerHTML = '<p style="color:var(--red)">Failed to load PR ' + escapeHtml(String(prId)) + '.</p>';
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function _renderPrDetail(pr) {
|
|
346
|
+
if (!pr) return '<p style="color:var(--muted)">No data.</p>';
|
|
347
|
+
if (pr._loading) {
|
|
348
|
+
return '<p style="color:var(--muted)">Loading PR ' + escapeHtml(String(pr.id || '')) + '...</p>';
|
|
349
|
+
}
|
|
350
|
+
const sq = pr.minionsReview || {};
|
|
351
|
+
const reviewLabel = sq.status === 'waiting' ? 'reviewing (minions)' : sq.status ? sq.status + ' (minions)' : (pr.reviewStatus || 'pending');
|
|
352
|
+
const reviewClass = (sq.status || pr.reviewStatus) === 'approved' ? 'approved' :
|
|
353
|
+
((sq.status || pr.reviewStatus) === 'changes-requested' || (sq.status || pr.reviewStatus) === 'rejected') ? 'rejected' :
|
|
354
|
+
(sq.status || pr.reviewStatus) === 'waiting' ? 'building' : 'draft';
|
|
355
|
+
const buildLabel = pr.buildStatus || 'none';
|
|
356
|
+
const buildClass = pr._buildStatusStale ? 'build-stale' :
|
|
357
|
+
pr.buildStatus === 'passing' ? 'build-pass' :
|
|
358
|
+
pr.buildStatus === 'failing' ? 'build-fail' :
|
|
359
|
+
pr.buildStatus === 'running' ? 'building' : 'no-build';
|
|
360
|
+
const statusLabel = pr.status || 'active';
|
|
361
|
+
const statusClass = statusLabel === 'merged' ? 'pr-merged' :
|
|
362
|
+
statusLabel === 'abandoned' ? 'pr-abandoned' :
|
|
363
|
+
statusLabel === 'draft' ? 'draft' : 'pr-active';
|
|
364
|
+
|
|
365
|
+
const badges =
|
|
366
|
+
'<span class="pr-badge ' + statusClass + '" title="status">' + escapeHtml(String(statusLabel)) + '</span> ' +
|
|
367
|
+
'<span class="pr-badge ' + reviewClass + '" title="review">' + escapeHtml(String(reviewLabel)) + '</span> ' +
|
|
368
|
+
'<span class="pr-badge ' + buildClass + '" title="build">' + escapeHtml(String(buildLabel)) + '</span>';
|
|
369
|
+
|
|
370
|
+
// Linked WIs — join via item._pr === pr.id (stamped by engine/queries.js).
|
|
371
|
+
const wis = (window._lastWorkItems) || [];
|
|
372
|
+
const linkedWis = wis.filter(w => w && w._pr === pr.id);
|
|
373
|
+
let linkedChips = '';
|
|
374
|
+
if (linkedWis.length && typeof renderArtifactLink === 'function') {
|
|
375
|
+
linkedChips = '<div style="margin-top:10px"><strong style="color:var(--muted);font-size:var(--text-base)">Linked Work Items:</strong><br>' +
|
|
376
|
+
linkedWis.map(w => renderArtifactLink({ type: 'wi', id: w.id, label: w.id, title: w.title || w.id })).join(' ') +
|
|
377
|
+
'</div>';
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// P-e6093f70 — PR ↔ PR linkage. Surface (a) a "Parent PR (follow-up)"
|
|
381
|
+
// chip when this PR record carries meta.pr_followup.parent_pr_url /
|
|
382
|
+
// parent_pr_id (mirrors the WI-side rendering at render-work-items.js
|
|
383
|
+
// _wiRenderDetail, P-79b47b0c) and (b) one chip per pr.depends_on[]
|
|
384
|
+
// entry. Both surfaces use renderArtifactLink({type:'pr'}) so click
|
|
385
|
+
// opens the parent / dep PR modal in-stack with browser-Back support.
|
|
386
|
+
//
|
|
387
|
+
// Graceful degradation: when parent_pr_url cannot be parsed into a
|
|
388
|
+
// canonical id (unknown host), fall back to a plain external <a>.
|
|
389
|
+
// depends_on chips consult the local PR cache (allPrs / window._lastPrs
|
|
390
|
+
// / window._lastPullRequests) and pass deleted:true when the dep is
|
|
391
|
+
// unknown so the chip renders struck-out (non-clickable) instead of
|
|
392
|
+
// silently linking to a 404.
|
|
393
|
+
let parentChip = '';
|
|
394
|
+
const followup = pr.meta && pr.meta.pr_followup;
|
|
395
|
+
if (followup && (followup.parent_pr_id || followup.parent_pr_url) && typeof renderArtifactLink === 'function') {
|
|
396
|
+
let parentPrId = followup.parent_pr_id || '';
|
|
397
|
+
if (!parentPrId && followup.parent_pr_url && typeof _wiDeriveCanonicalPrIdFromUrl === 'function') {
|
|
398
|
+
parentPrId = _wiDeriveCanonicalPrIdFromUrl(followup.parent_pr_url) || '';
|
|
399
|
+
}
|
|
400
|
+
if (parentPrId) {
|
|
401
|
+
parentChip = '<div style="margin-top:10px"><strong style="color:var(--muted);font-size:var(--text-base)">Parent PR (follow-up):</strong> ' +
|
|
402
|
+
renderArtifactLink({ type: 'pr', id: parentPrId, label: parentPrId, title: 'Follow-up from ' + (followup.parent_pr_url || parentPrId) }) +
|
|
403
|
+
'</div>';
|
|
404
|
+
} else if (followup.parent_pr_url) {
|
|
405
|
+
parentChip = '<div style="margin-top:10px"><strong style="color:var(--muted);font-size:var(--text-base)">Parent PR (follow-up):</strong> ' +
|
|
406
|
+
'<a href="' + escapeHtml(followup.parent_pr_url) + '" target="_blank" rel="noopener" style="color:var(--blue)">' + escapeHtml(followup.parent_pr_url) + '</a>' +
|
|
407
|
+
'</div>';
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let dependsChips = '';
|
|
412
|
+
if (Array.isArray(pr.depends_on) && pr.depends_on.length && typeof renderArtifactLink === 'function') {
|
|
413
|
+
const knownPrIds = new Set();
|
|
414
|
+
const knownSources = [
|
|
415
|
+
(typeof allPrs !== 'undefined' && Array.isArray(allPrs)) ? allPrs : null,
|
|
416
|
+
window._lastPrs,
|
|
417
|
+
window._lastPullRequests,
|
|
418
|
+
];
|
|
419
|
+
for (const src of knownSources) {
|
|
420
|
+
if (Array.isArray(src)) for (const p of src) { if (p && p.id) knownPrIds.add(p.id); }
|
|
421
|
+
}
|
|
422
|
+
dependsChips = '<div style="margin-top:10px"><strong style="color:var(--muted);font-size:var(--text-base)">Depends On:</strong> ' +
|
|
423
|
+
pr.depends_on.map(function(d) {
|
|
424
|
+
const depId = String(d);
|
|
425
|
+
const isDeleted = knownPrIds.size > 0 && !knownPrIds.has(depId);
|
|
426
|
+
return renderArtifactLink({
|
|
427
|
+
type: 'pr',
|
|
428
|
+
id: depId,
|
|
429
|
+
label: depId,
|
|
430
|
+
title: isDeleted ? 'Dependency PR ' + depId + ' is no longer in local state' : 'Depends on ' + depId,
|
|
431
|
+
deleted: isDeleted,
|
|
432
|
+
});
|
|
433
|
+
}).join(' ') +
|
|
434
|
+
'</div>';
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Reviewers list
|
|
438
|
+
let reviewers = '';
|
|
439
|
+
if (Array.isArray(pr.reviewers) && pr.reviewers.length) {
|
|
440
|
+
reviewers = '<div style="margin-top:6px"><strong style="color:var(--muted);font-size:var(--text-base)">Reviewers:</strong> ' +
|
|
441
|
+
pr.reviewers.map(r => '<code>' + escapeHtml(String(typeof r === 'string' ? r : (r.displayName || r.name || ''))) + '</code>').join(' ') +
|
|
442
|
+
'</div>';
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const agent = pr.agent ? '<div><strong style="color:var(--muted);font-size:var(--text-base)">Agent:</strong> ' + escapeHtml(String(pr.agent)) + '</div>' : '';
|
|
446
|
+
const branch = pr.branch ? '<div><strong style="color:var(--muted);font-size:var(--text-base)">Branch:</strong> <code>' + escapeHtml(String(pr.branch)) + '</code></div>' : '';
|
|
447
|
+
const created = pr.created ? '<div><strong style="color:var(--muted);font-size:var(--text-base)">Created:</strong> ' + escapeHtml(String(pr.created)) + '</div>' : '';
|
|
448
|
+
const project = pr._project ? '<div><strong style="color:var(--muted);font-size:var(--text-base)">Project:</strong> ' + escapeHtml(String(pr._project)) + '</div>' : '';
|
|
449
|
+
|
|
450
|
+
const descSafe = typeof renderMd === 'function' ? renderMd(pr.description || '') : escapeHtml(pr.description || '');
|
|
451
|
+
const descBlock = pr.description ?
|
|
452
|
+
'<div style="margin-top:14px"><strong style="color:var(--muted);font-size:var(--text-base)">Description:</strong><div style="margin-top:6px;font-size:var(--text-md);line-height:1.6">' + descSafe + '</div></div>' :
|
|
453
|
+
'';
|
|
454
|
+
|
|
455
|
+
const openOnHost = pr.url ?
|
|
456
|
+
'<a class="btn-secondary" style="display:inline-block;margin-top:14px;padding:6px 12px;text-decoration:none" href="' + escapeHtml(safeUrl(pr.url)) + '" target="_blank" rel="noopener">Open on GitHub ↗</a>' :
|
|
457
|
+
'';
|
|
458
|
+
|
|
459
|
+
return '<div style="font-family:\'Segoe UI\',system-ui,sans-serif">' +
|
|
460
|
+
'<div style="margin-bottom:10px">' +
|
|
461
|
+
'<code style="color:var(--muted);font-size:var(--text-base)">' + escapeHtml(String(pr.id || '')) + '</code><br>' +
|
|
462
|
+
badges +
|
|
463
|
+
'</div>' +
|
|
464
|
+
agent + branch + project + created +
|
|
465
|
+
reviewers +
|
|
466
|
+
linkedChips +
|
|
467
|
+
parentChip +
|
|
468
|
+
dependsChips +
|
|
469
|
+
descBlock +
|
|
470
|
+
openOnHost +
|
|
471
|
+
'</div>';
|
|
472
|
+
}
|
|
473
|
+
|
|
303
474
|
function openModal(i) {
|
|
304
475
|
const item = inboxData[i];
|
|
305
476
|
if (!item) return;
|
|
@@ -490,4 +661,4 @@ async function resumePausedCause(btn) {
|
|
|
490
661
|
}
|
|
491
662
|
}
|
|
492
663
|
|
|
493
|
-
window.MinionsPrs = { prRow, prTableHtml, renderPrs, prPrev, prNext, openAllPrs, openModal, openAddPrModal, unlinkPr, togglePrObserve, resumePausedCause };
|
|
664
|
+
window.MinionsPrs = { prRow, prTableHtml, renderPrs, prPrev, prNext, openAllPrs, openModal, openAddPrModal, openPrDetail, unlinkPr, togglePrObserve, resumePausedCause };
|
|
@@ -405,6 +405,18 @@ function openScheduleDetail(id) {
|
|
|
405
405
|
'<div><strong style="color:var(--muted)">Agent:</strong> ' + escHtml(s.agent || 'auto') + '</div>' +
|
|
406
406
|
'<div><strong style="color:var(--muted)">Status:</strong> ' + enabledLabel + '</div>' +
|
|
407
407
|
'<div><strong style="color:var(--muted)">Last Run:</strong> ' + escHtml(lastRun) + '</div>' +
|
|
408
|
+
// P-c549d07e — "Recent dispatches" chip row. Each chip routes through
|
|
409
|
+
// renderArtifactLink({type:'wi'}) → openArtifact() so clicking pushes
|
|
410
|
+
// the WI detail modal onto the stack. Hard-cap to 5 chips even if the
|
|
411
|
+
// API returned more (defensive — the API already slices).
|
|
412
|
+
(Array.isArray(s._recentWorkItemIds) && s._recentWorkItemIds.length > 0
|
|
413
|
+
? '<div><strong style="color:var(--muted)">Recent dispatches:</strong>' +
|
|
414
|
+
'<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px">' +
|
|
415
|
+
s._recentWorkItemIds.slice(0, 5).map(function(wi) {
|
|
416
|
+
return renderArtifactLink({ type: 'wi', id: String(wi), label: String(wi), title: 'Open work item ' + wi });
|
|
417
|
+
}).join('') +
|
|
418
|
+
'</div></div>'
|
|
419
|
+
: '') +
|
|
408
420
|
(s.description ? '<div><strong style="color:var(--muted)">Description:</strong><div style="margin-top:4px;padding:8px;background:var(--surface2);border-radius:4px">' + renderMd(s.description) + '</div></div>' : '') +
|
|
409
421
|
'</div>';
|
|
410
422
|
|