@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
|
@@ -575,4 +575,154 @@ function restoreDashboardScrollState(root, state) {
|
|
|
575
575
|
});
|
|
576
576
|
}
|
|
577
577
|
|
|
578
|
-
|
|
578
|
+
// P-ce1e5e47 — universal artifact-modal dispatcher. Chip clicks (and the
|
|
579
|
+
// URL-hash replay path) call openArtifact(type, id); this pushes a stack
|
|
580
|
+
// frame and delegates to the existing per-type opener.
|
|
581
|
+
//
|
|
582
|
+
// Type → handler table:
|
|
583
|
+
// wi → openWorkItemDetail(id)
|
|
584
|
+
// pr → openPrDetail(id) (P-79b47b0c; falls back to openPrUrl)
|
|
585
|
+
// plan/prd → planView(id)
|
|
586
|
+
// note → openInboxNote(id)
|
|
587
|
+
// kb → kbOpenItem(category, file) (id = "category/file")
|
|
588
|
+
// schedule → openScheduleDetail(id)
|
|
589
|
+
// watch → openWatchDetail(id)
|
|
590
|
+
// pipeline → openPipelineDetail(id)
|
|
591
|
+
// meeting → openMeetingDetail(id)
|
|
592
|
+
// agent → openAgentDetail(id) ← detail-panel exception (see below)
|
|
593
|
+
//
|
|
594
|
+
// P-30b6cf8a — Detail-panel exception (intentional, documented). The agent
|
|
595
|
+
// type does NOT push a modal-stack frame and does NOT use the modal shell.
|
|
596
|
+
// openAgentDetail() opens the right-side slide-in detail panel (tabs +
|
|
597
|
+
// live-stream + charter editor) which is a peer of the modal stack, not a
|
|
598
|
+
// member of it. Clicking an agent chip from inside a modal therefore:
|
|
599
|
+
// 1. Resets the modal stack (resetModalStack) and physically closes the
|
|
600
|
+
// modal shell — modal-stack identity goes back to empty.
|
|
601
|
+
// 2. Opens the agent detail panel.
|
|
602
|
+
// The reverse direction is symmetric: clicking a WI/PR/plan chip from
|
|
603
|
+
// inside the panel calls openArtifact('<type>', id), which (per the
|
|
604
|
+
// non-agent branch below) closes the detail panel and starts a fresh
|
|
605
|
+
// modal stack. This one-way exception is the deliberate contract — do
|
|
606
|
+
// NOT "fix" it by adding pushModalFrame for the agent type. See the
|
|
607
|
+
// matching JSDoc on dashboard/js/render-agents.js openAgentDetail().
|
|
608
|
+
function openArtifact(type, id) {
|
|
609
|
+
if (!type || id === undefined || id === null) return;
|
|
610
|
+
if (type === 'agent') {
|
|
611
|
+
if (typeof resetModalStack === 'function') resetModalStack();
|
|
612
|
+
if (typeof _physicallyCloseModal === 'function') _physicallyCloseModal();
|
|
613
|
+
if (typeof openAgentDetail === 'function') openAgentDetail(id);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
// Non-agent (modal) type: dismiss the detail panel if it's open so the
|
|
617
|
+
// modal-stack peer cleanly takes over the foreground.
|
|
618
|
+
try {
|
|
619
|
+
var panelEl = document.getElementById('detail-panel');
|
|
620
|
+
if (panelEl && panelEl.classList.contains('open') && typeof closeDetail === 'function') {
|
|
621
|
+
closeDetail();
|
|
622
|
+
}
|
|
623
|
+
} catch { /* DOM may not be ready in unit-test eval */ }
|
|
624
|
+
if (typeof pushModalFrame === 'function') {
|
|
625
|
+
try {
|
|
626
|
+
pushModalFrame({ type: type, id: String(id), openFn: openArtifact, openArgs: [type, String(id)] });
|
|
627
|
+
} catch (e) { try { console.error('pushModalFrame failed', e); } catch {} }
|
|
628
|
+
}
|
|
629
|
+
switch (type) {
|
|
630
|
+
case 'wi':
|
|
631
|
+
if (typeof openWorkItemDetail === 'function') openWorkItemDetail(id);
|
|
632
|
+
break;
|
|
633
|
+
case 'pr':
|
|
634
|
+
if (typeof openPrDetail === 'function') openPrDetail(id);
|
|
635
|
+
else if (typeof window !== 'undefined' && window.MinionsPrs && typeof window.MinionsPrs.openPrDetail === 'function') window.MinionsPrs.openPrDetail(id);
|
|
636
|
+
break;
|
|
637
|
+
case 'plan':
|
|
638
|
+
case 'prd':
|
|
639
|
+
if (typeof planView === 'function') planView(id);
|
|
640
|
+
break;
|
|
641
|
+
case 'note':
|
|
642
|
+
if (typeof openInboxNote === 'function') openInboxNote(id);
|
|
643
|
+
break;
|
|
644
|
+
case 'kb': {
|
|
645
|
+
var idx = String(id).indexOf('/');
|
|
646
|
+
if (idx > 0 && typeof kbOpenItem === 'function') {
|
|
647
|
+
kbOpenItem(String(id).slice(0, idx), String(id).slice(idx + 1));
|
|
648
|
+
}
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
651
|
+
case 'schedule':
|
|
652
|
+
if (typeof openScheduleDetail === 'function') openScheduleDetail(id);
|
|
653
|
+
break;
|
|
654
|
+
case 'watch':
|
|
655
|
+
if (typeof openWatchDetail === 'function') openWatchDetail(id);
|
|
656
|
+
break;
|
|
657
|
+
case 'pipeline':
|
|
658
|
+
if (typeof openPipelineDetail === 'function') openPipelineDetail(id);
|
|
659
|
+
break;
|
|
660
|
+
case 'meeting':
|
|
661
|
+
if (typeof openMeetingDetail === 'function') openMeetingDetail(id);
|
|
662
|
+
break;
|
|
663
|
+
default:
|
|
664
|
+
try { console.warn('openArtifact: unknown type', type); } catch {}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// P-e265cd31 — shared artifact-chip renderer. Every chip click routes through
|
|
669
|
+
// openArtifact() so the modal stack push, URL-hash update, and browser-Back
|
|
670
|
+
// integration come for free at every callsite (no more inline
|
|
671
|
+
// pushModalBack(...);openXxxDetail(...) string-built pills).
|
|
672
|
+
//
|
|
673
|
+
// Icon table — keep consistent across the dashboard:
|
|
674
|
+
// wi=📋 pr=🔀 plan=📋 prd=📄 note=📝 kb=📚
|
|
675
|
+
// schedule=⏰ watch=👁 pipeline=⛓ meeting=🗣 agent=🤖
|
|
676
|
+
//
|
|
677
|
+
// Options:
|
|
678
|
+
// type — one of the icon-table keys (case-sensitive)
|
|
679
|
+
// id — artifact identifier; passed verbatim to openArtifact(type, id)
|
|
680
|
+
// label — visible chip text (defaults to id)
|
|
681
|
+
// title — optional hover tooltip (defaults to empty)
|
|
682
|
+
// icon — optional icon override (defaults to ICON_TABLE[type])
|
|
683
|
+
// deleted — true → render struck-out, non-clickable chip so dead refs
|
|
684
|
+
// still show context to the operator
|
|
685
|
+
//
|
|
686
|
+
// Unknown type with no explicit icon falls back to a plain escaped span (no
|
|
687
|
+
// chip styling, no throw) so callers can pass through arbitrary refs safely.
|
|
688
|
+
var _ARTIFACT_ICON_TABLE = {
|
|
689
|
+
wi: '📋', pr: '🔀', plan: '📋', prd: '📄', note: '📝', kb: '📚',
|
|
690
|
+
schedule: '⏰', watch: '👁', pipeline: '⛓', meeting: '🗣', agent: '🤖',
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
function renderArtifactLink(opts) {
|
|
694
|
+
var o = opts || {};
|
|
695
|
+
var type = String(o.type == null ? '' : o.type);
|
|
696
|
+
var id = o.id == null ? '' : String(o.id);
|
|
697
|
+
var label = o.label == null ? id : String(o.label);
|
|
698
|
+
var title = o.title == null ? '' : String(o.title);
|
|
699
|
+
var hasIconOverride = typeof o.icon === 'string' && o.icon.length > 0;
|
|
700
|
+
var icon = hasIconOverride ? o.icon : (_ARTIFACT_ICON_TABLE[type] || '');
|
|
701
|
+
var deleted = o.deleted === true;
|
|
702
|
+
|
|
703
|
+
if (!Object.prototype.hasOwnProperty.call(_ARTIFACT_ICON_TABLE, type) && !hasIconOverride) {
|
|
704
|
+
return '<span class="artifact-chip-unknown">' + escHtml(label) + '</span>';
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
var classes = 'artifact-chip' + (deleted ? ' deleted' : '');
|
|
708
|
+
// JSON.stringify produces a safe JS string literal (handles backslashes,
|
|
709
|
+
// quotes, control chars). escHtml then makes it safe for the attribute
|
|
710
|
+
// value; the HTML parser reverses the entity escapes before the JS
|
|
711
|
+
// evaluator sees them, so the call site always receives the original
|
|
712
|
+
// string values.
|
|
713
|
+
var clickAttr = deleted
|
|
714
|
+
? ''
|
|
715
|
+
: ' onclick="event.stopPropagation();openArtifact('
|
|
716
|
+
+ escHtml(JSON.stringify(type)) + ','
|
|
717
|
+
+ escHtml(JSON.stringify(id)) + ')"';
|
|
718
|
+
var titleAttr = title ? ' title="' + escHtml(title) + '"' : '';
|
|
719
|
+
return '<span class="' + classes + '"'
|
|
720
|
+
+ ' data-art-type="' + escHtml(type) + '"'
|
|
721
|
+
+ ' data-art-id="' + escHtml(id) + '"'
|
|
722
|
+
+ clickAttr + titleAttr + '>'
|
|
723
|
+
+ (icon ? '<span class="artifact-chip-icon">' + escHtml(icon) + '</span>' : '')
|
|
724
|
+
+ '<span class="artifact-chip-label">' + escHtml(label) + '</span>'
|
|
725
|
+
+ '</span>';
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
window.MinionsRenderUtils = { formatToolSummary, renderAgentOutput, renderTerminalBanner, renderPager, pinButton, captureDashboardScrollState, restoreDashboardScrollState, openArtifact, renderArtifactLink };
|
|
@@ -101,6 +101,48 @@ function _targetTypeLabel(type) {
|
|
|
101
101
|
return _WATCH_TARGET_LABELS[type] || (type || '');
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// P-1acd4cde — map a watch.targetType to the artifact-chip type understood by
|
|
105
|
+
// renderArtifactLink / openArtifact (defined in render-utils.js). Returns null
|
|
106
|
+
// for targetTypes that don't correspond to a navigable artifact (teams-channel,
|
|
107
|
+
// future plugin types) so callers fall back to plain text rendering. The
|
|
108
|
+
// mapping covers the 8 first-class TARGET_TYPES registered in engine/watches.js
|
|
109
|
+
// (engine/shared.js#WATCH_TARGET_TYPE) — dispatch ids are WI ids in practice
|
|
110
|
+
// so they share the wi chip target.
|
|
111
|
+
var _WATCH_TARGET_TO_ART_TYPE = {
|
|
112
|
+
'pr': 'pr',
|
|
113
|
+
'work-item': 'wi',
|
|
114
|
+
'meeting': 'meeting',
|
|
115
|
+
'plan': 'plan',
|
|
116
|
+
'schedule': 'schedule',
|
|
117
|
+
'pipeline': 'pipeline',
|
|
118
|
+
'dispatch': 'wi',
|
|
119
|
+
'agent': 'agent',
|
|
120
|
+
};
|
|
121
|
+
function _watchTargetToArtType(targetType) {
|
|
122
|
+
if (!targetType) return null;
|
|
123
|
+
return _WATCH_TARGET_TO_ART_TYPE[targetType] || null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// P-1acd4cde — render the watch target as a clickable artifact chip when the
|
|
127
|
+
// targetType maps to a known artifact AND the target is a non-empty string id.
|
|
128
|
+
// Object targets (e.g. teams-channel `{teamId, channelId}`) and plugin
|
|
129
|
+
// targetTypes without an artifact equivalent fall back to the legacy
|
|
130
|
+
// _formatWatchTarget plain text so the W-mq1j5f9z00030b8f title-fallback
|
|
131
|
+
// contract still holds for non-artifact watches.
|
|
132
|
+
function _renderWatchTargetChip(w) {
|
|
133
|
+
var artType = _watchTargetToArtType(w && w.targetType);
|
|
134
|
+
if (artType && typeof w.target === 'string' && w.target.length > 0
|
|
135
|
+
&& typeof renderArtifactLink === 'function') {
|
|
136
|
+
return renderArtifactLink({
|
|
137
|
+
type: artType,
|
|
138
|
+
id: w.target,
|
|
139
|
+
label: w.target,
|
|
140
|
+
title: _targetTypeLabel(w.targetType) + ' ' + w.target,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return escHtml(_formatWatchTarget(w && w.target, w && w.targetType));
|
|
144
|
+
}
|
|
145
|
+
|
|
104
146
|
// W-mq1j5f9z00030b8f — `teams-channel` target is `{teamId, channelId}` (object);
|
|
105
147
|
// other targetTypes use strings. Avoid `String(target)` → "[object Object]";
|
|
106
148
|
// route all target rendering here so future object-target plugins inherit safety.
|
|
@@ -339,7 +381,7 @@ function openWatchDetail(id) {
|
|
|
339
381
|
|
|
340
382
|
var body = '<div style="display:flex;flex-direction:column;gap:10px;font-size:var(--text-md);line-height:1.6">' +
|
|
341
383
|
'<div><strong style="color:var(--muted)">ID:</strong> ' + escHtml(w.id) + '</div>' +
|
|
342
|
-
'<div><strong style="color:var(--muted)">Target:</strong> ' +
|
|
384
|
+
'<div><strong style="color:var(--muted)">Target:</strong> ' + _renderWatchTargetChip(w) + '</div>' +
|
|
343
385
|
'<div><strong style="color:var(--muted)">Target Type:</strong> <span class="dispatch-type explore">' + escHtml(targetLabel) + '</span></div>' +
|
|
344
386
|
'<div><strong style="color:var(--muted)">Condition:</strong> <span style="color:var(--blue)">' + escHtml(condLabel) + '</span></div>' +
|
|
345
387
|
'<div><strong style="color:var(--muted)">Check Interval:</strong> ' + escHtml(_intervalToHuman(w.interval)) + '</div>' +
|
|
@@ -356,7 +398,7 @@ function openWatchDetail(id) {
|
|
|
356
398
|
// P-w14e7a8c — Phase 7.1: render cross-target requirements list when
|
|
357
399
|
// the watch carries a non-empty requires[] from the editor.
|
|
358
400
|
_renderWatchRequiresDetail(w.requires) +
|
|
359
|
-
(w._lastActionResult ? '<div><strong style="color:var(--muted)">Last Action Result:</strong> <span style="color:' + (w._lastActionResult.ok ? 'var(--green)' : 'var(--red)') + '">' + (w._lastActionResult.ok ? 'OK' : 'FAILED') + '</span> — ' + escHtml(w._lastActionResult.summary || '') + (w._lastActionResult.dispatchedItemId ? ' (
|
|
401
|
+
(w._lastActionResult ? '<div><strong style="color:var(--muted)">Last Action Result:</strong> <span style="color:' + (w._lastActionResult.ok ? 'var(--green)' : 'var(--red)') + '">' + (w._lastActionResult.ok ? 'OK' : 'FAILED') + '</span> — ' + escHtml(w._lastActionResult.summary || '') + (w._lastActionResult.dispatchedItemId && typeof renderArtifactLink === 'function' ? ' ' + renderArtifactLink({ type: 'wi', id: String(w._lastActionResult.dispatchedItemId), label: String(w._lastActionResult.dispatchedItemId), title: 'Dispatched work item ' + w._lastActionResult.dispatchedItemId }) : '') + '</div>' : '') +
|
|
360
402
|
'<div><strong style="color:var(--muted)">Created:</strong> ' + escHtml(createdAt) + '</div>' +
|
|
361
403
|
'<div><strong style="color:var(--muted)">Last Checked:</strong> ' + escHtml(lastChecked) + '</div>' +
|
|
362
404
|
'<div><strong style="color:var(--muted)">Last Triggered:</strong> ' + escHtml(lastTriggered) + '</div>' +
|
|
@@ -389,13 +431,22 @@ function openWatchDetail(id) {
|
|
|
389
431
|
return res.json();
|
|
390
432
|
}).then(function(data) {
|
|
391
433
|
if (!container.isConnected) return; // user closed the modal
|
|
392
|
-
//
|
|
393
|
-
|
|
434
|
+
// P-ce1e5e47 — skip the history render when the user has stacked
|
|
435
|
+
// another modal on top of this watch view.
|
|
436
|
+
var apply = function() {
|
|
437
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: _renderWatchHistoryDetail() escapes all user-controlled fields before assembling HTML
|
|
438
|
+
container.innerHTML = _renderWatchHistoryDetail(data && data.history);
|
|
439
|
+
};
|
|
440
|
+
if (typeof withTopFrame === 'function') withTopFrame('watch', w.id, apply);
|
|
441
|
+
else apply();
|
|
394
442
|
}).catch(function() {
|
|
395
443
|
if (!container.isConnected) return;
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
444
|
+
var applyFallback = function() {
|
|
445
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: _renderWatchHistoryDetail() escapes all user-controlled fields before assembling HTML
|
|
446
|
+
container.innerHTML = _renderWatchHistoryDetail(w._history || []);
|
|
447
|
+
};
|
|
448
|
+
if (typeof withTopFrame === 'function') withTopFrame('watch', w.id, applyFallback);
|
|
449
|
+
else applyFallback();
|
|
399
450
|
});
|
|
400
451
|
})();
|
|
401
452
|
}
|
|
@@ -824,4 +875,7 @@ window.MinionsWatches = {
|
|
|
824
875
|
// W-mq1j5f9z00030b8f — target / title fallback helpers exposed for unit coverage.
|
|
825
876
|
_formatWatchTarget: _formatWatchTarget,
|
|
826
877
|
_watchTitleFallback: _watchTitleFallback,
|
|
878
|
+
// P-1acd4cde — watch ↔ target artifact-chip linkage helpers exposed for unit coverage.
|
|
879
|
+
_watchTargetToArtType: _watchTargetToArtType,
|
|
880
|
+
_renderWatchTargetChip: _renderWatchTargetChip,
|
|
827
881
|
};
|
|
@@ -668,6 +668,19 @@ async function _submitCreateWorkItem(e) {
|
|
|
668
668
|
} catch (e) { alert('Error: ' + e.message); openCreateWorkItemModal(); }
|
|
669
669
|
}
|
|
670
670
|
|
|
671
|
+
// P-79b47b0c — Derive a canonical PR id (`<host>:<slug>#<number>`) from a
|
|
672
|
+
// GitHub or ADO PR URL. Matches the format produced by engine/queries.js
|
|
673
|
+
// when stamping pr.id. Returns null when the URL doesn't match a known
|
|
674
|
+
// host pattern.
|
|
675
|
+
function _wiDeriveCanonicalPrIdFromUrl(url) {
|
|
676
|
+
if (!url || typeof url !== 'string') return null;
|
|
677
|
+
var ghMatch = url.match(/https?:\/\/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/i);
|
|
678
|
+
if (ghMatch) return 'github:' + ghMatch[1] + '#' + ghMatch[2];
|
|
679
|
+
var adoMatch = url.match(/https?:\/\/dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/?]+)\/pullrequest\/(\d+)/i);
|
|
680
|
+
if (adoMatch) return 'ado:' + adoMatch[1] + '/' + adoMatch[2] + '/' + adoMatch[3] + '#' + adoMatch[4];
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
|
|
671
684
|
function _wiRenderDetail(item) {
|
|
672
685
|
const field = (label, value) => value ? '<div style="margin-bottom:8px"><span style="color:var(--muted);font-size:var(--text-sm);text-transform:uppercase;letter-spacing:0.5px">' + label + '</span><div style="margin-top:2px">' + value + '</div></div>' : '';
|
|
673
686
|
const badge = (cls, text) => '<span class="pr-badge ' + cls + '">' + escapeHtml(text) + '</span>';
|
|
@@ -753,8 +766,18 @@ function _wiRenderDetail(item) {
|
|
|
753
766
|
// Defensive: CC dispatches can land here with these fields as strings
|
|
754
767
|
// (e.g. acceptanceCriteria: "fix the login bug"). Coerce to arrays so
|
|
755
768
|
// .map() doesn't throw and crash the modal.
|
|
769
|
+
// P-79b47b0c — depends_on[] now renders as WI chips routed through
|
|
770
|
+
// renderArtifactLink (was bare <code> spans). Each chip clicks into the
|
|
771
|
+
// dependency's WI detail modal in-stack.
|
|
756
772
|
var deps = Array.isArray(item.depends_on) ? item.depends_on : [];
|
|
757
|
-
if (deps.length)
|
|
773
|
+
if (deps.length) {
|
|
774
|
+
html += field('Depends On', deps.map(d => renderArtifactLink({ type: 'wi', id: String(d), label: String(d), title: 'Depends on ' + d })).join(' '));
|
|
775
|
+
}
|
|
776
|
+
// P-79b47b0c — parent_id chip (when present). Surfaces the parent WI that
|
|
777
|
+
// decomposed/spawned this child item.
|
|
778
|
+
if (item.parent_id) {
|
|
779
|
+
html += field('Parent Work Item', renderArtifactLink({ type: 'wi', id: String(item.parent_id), label: String(item.parent_id), title: 'Parent: ' + item.parent_id }));
|
|
780
|
+
}
|
|
758
781
|
var ac = Array.isArray(item.acceptanceCriteria)
|
|
759
782
|
? item.acceptanceCriteria
|
|
760
783
|
: (typeof item.acceptanceCriteria === 'string' && item.acceptanceCriteria.trim()
|
|
@@ -774,38 +797,114 @@ function _wiRenderDetail(item) {
|
|
|
774
797
|
html += field('References', '<span style="color:var(--muted)">' + item.referencesCount + ' reference(s) — loading…</span>');
|
|
775
798
|
}
|
|
776
799
|
if (item._humanFeedback) html += field('Human Feedback', (item._humanFeedback.rating === 'up' ? '👍' : '👎') + (item._humanFeedback.comment ? ' — ' + escapeHtml(item._humanFeedback.comment) : ''));
|
|
777
|
-
|
|
800
|
+
// P-79b47b0c — PR chip routed through renderArtifactLink so click opens the
|
|
801
|
+
// PR detail modal in-stack (was a raw <a target="_blank">).
|
|
802
|
+
if (item._pr) html += field('Pull Request', renderArtifactLink({ type: 'pr', id: item._pr, label: item._pr, title: item._prUrl || item._pr }));
|
|
803
|
+
// P-79b47b0c — follow-up parent PR chip. WIs spawned by PR review/build-fix
|
|
804
|
+
// dispatchers stamp meta.pr_followup.{parent_pr_url, parent_pr_id} on the
|
|
805
|
+
// child item; surface that lineage as a clickable in-stack PR chip. Prefer
|
|
806
|
+
// parent_pr_id when stamped; otherwise parse the URL into a canonical id.
|
|
807
|
+
var followup = item.meta && item.meta.pr_followup;
|
|
808
|
+
if (followup && (followup.parent_pr_id || followup.parent_pr_url)) {
|
|
809
|
+
var parentPrId = followup.parent_pr_id || _wiDeriveCanonicalPrIdFromUrl(followup.parent_pr_url);
|
|
810
|
+
if (parentPrId) {
|
|
811
|
+
html += field('Parent PR (follow-up)', renderArtifactLink({ type: 'pr', id: parentPrId, label: parentPrId, title: 'Follow-up from ' + (followup.parent_pr_url || parentPrId) }));
|
|
812
|
+
} else if (followup.parent_pr_url) {
|
|
813
|
+
// Couldn't derive a canonical id (unknown host?). Fall back to plain link.
|
|
814
|
+
html += field('Parent PR (follow-up)', '<a href="' + escapeHtml(followup.parent_pr_url) + '" target="_blank" rel="noopener" style="color:var(--blue)">' + escapeHtml(followup.parent_pr_url) + '</a>');
|
|
815
|
+
}
|
|
816
|
+
}
|
|
778
817
|
|
|
779
|
-
//
|
|
818
|
+
// P-c549d07e — "Spawned by" back-link chip. The schedule/pipeline that
|
|
819
|
+
// dispatched this WI is the natural Back target from the WI modal. The
|
|
820
|
+
// primary source is meta.spawnedBy ('schedule:<id>' | 'pipeline:<id>'),
|
|
821
|
+
// stamped by engine/scheduler.js + engine/pipeline.js on new dispatches.
|
|
822
|
+
// For older WIs already in state we fall back to (a) item._scheduleId
|
|
823
|
+
// and (b) item.createdBy when it starts with 'pipeline:' so historical
|
|
824
|
+
// items still light up the chip — purely additive, no behavior change.
|
|
825
|
+
(function() {
|
|
826
|
+
var meta = item.meta || {};
|
|
827
|
+
var spawnedBy = typeof meta.spawnedBy === 'string' ? meta.spawnedBy : '';
|
|
828
|
+
if (!spawnedBy && item._scheduleId) spawnedBy = 'schedule:' + item._scheduleId;
|
|
829
|
+
if (!spawnedBy && typeof item.createdBy === 'string' && item.createdBy.indexOf('pipeline:') === 0) {
|
|
830
|
+
spawnedBy = item.createdBy;
|
|
831
|
+
}
|
|
832
|
+
if (!spawnedBy) return;
|
|
833
|
+
var colonIdx = spawnedBy.indexOf(':');
|
|
834
|
+
if (colonIdx <= 0) return;
|
|
835
|
+
var kind = spawnedBy.slice(0, colonIdx);
|
|
836
|
+
var dispatcherId = spawnedBy.slice(colonIdx + 1);
|
|
837
|
+
if (!dispatcherId) return;
|
|
838
|
+
var chip;
|
|
839
|
+
if (kind === 'schedule') {
|
|
840
|
+
chip = renderArtifactLink({ type: 'schedule', id: dispatcherId, label: dispatcherId, title: 'Open schedule ' + dispatcherId });
|
|
841
|
+
} else if (kind === 'pipeline') {
|
|
842
|
+
chip = renderArtifactLink({ type: 'pipeline', id: dispatcherId, label: dispatcherId, title: 'Open pipeline ' + dispatcherId });
|
|
843
|
+
} else {
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
html += field('Spawned by', chip);
|
|
847
|
+
})();
|
|
848
|
+
|
|
849
|
+
// Artifacts — branch (non-navigation) + navigation chips routed through
|
|
850
|
+
// renderArtifactLink so chip click → openArtifact() → modal-stack push +
|
|
851
|
+
// URL hash + browser-Back integration come for free (P-e265cd31).
|
|
852
|
+
// P-79b47b0c — also surface item._source (the WI's raw source file) as a
|
|
853
|
+
// dedicated chip when it isn't already covered by arts.plan / arts.prd /
|
|
854
|
+
// arts.sourcePlan. Chip type is inferred from the file extension (.md →
|
|
855
|
+
// plan, .json → prd).
|
|
780
856
|
var arts = item._artifacts || {};
|
|
781
857
|
var artPills = '';
|
|
782
|
-
var
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
if (arts.
|
|
786
|
-
if (arts.
|
|
787
|
-
|
|
788
|
-
|
|
858
|
+
var branchStyle = 'display:inline-flex;align-items:center;gap:3px;padding:2px 8px;border-radius:10px;font-size:var(--text-sm);background:var(--surface2);border:1px solid var(--border);color:var(--text);cursor:default';
|
|
859
|
+
if (arts.branch) artPills += '<span style="' + branchStyle + '">🌿 ' + escapeHtml(arts.branch) + '</span> ';
|
|
860
|
+
if (arts.plan) artPills += renderArtifactLink({ type: 'plan', id: arts.plan, label: 'Plan' }) + ' ';
|
|
861
|
+
if (arts.prd) artPills += renderArtifactLink({ type: 'prd', id: arts.prd, label: 'PRD' }) + ' ';
|
|
862
|
+
if (arts.sourcePlan) artPills += renderArtifactLink({ type: 'plan', id: arts.sourcePlan, label: 'Source Plan' }) + ' ';
|
|
863
|
+
// Source chip (item._source) — render when it's a file path that isn't
|
|
864
|
+
// already represented in arts.plan / arts.prd / arts.sourcePlan.
|
|
865
|
+
if (item._source && typeof item._source === 'string' && item._source !== 'central') {
|
|
866
|
+
var sourceAlreadyShown = arts.plan === item._source || arts.prd === item._source || arts.sourcePlan === item._source;
|
|
867
|
+
if (!sourceAlreadyShown) {
|
|
868
|
+
var sourceType = item._source.endsWith('.json') ? 'prd' : 'plan';
|
|
869
|
+
artPills += renderArtifactLink({ type: sourceType, id: item._source, label: 'Source', title: item._source }) + ' ';
|
|
870
|
+
}
|
|
871
|
+
}
|
|
789
872
|
if (arts.notes && arts.notes.length > 0) {
|
|
790
873
|
arts.notes.forEach(function(n) {
|
|
791
874
|
var noteFile = (n && typeof n === 'object') ? (n.file || n) : String(n || '');
|
|
792
875
|
if (noteFile.startsWith('kb:')) {
|
|
793
|
-
var
|
|
794
|
-
var
|
|
795
|
-
|
|
876
|
+
var kbBody = noteFile.slice(3);
|
|
877
|
+
var slashIdx = kbBody.indexOf('/');
|
|
878
|
+
if (slashIdx <= 0) return;
|
|
879
|
+
var kbFile = kbBody.slice(slashIdx + 1);
|
|
796
880
|
var kbLabel = kbFile.replace(/\.md$/, '').slice(0, 30);
|
|
797
|
-
artPills +=
|
|
881
|
+
artPills += renderArtifactLink({ type: 'kb', id: kbBody, label: kbLabel, title: 'KB: ' + kbFile }) + ' ';
|
|
798
882
|
} else if (noteFile.startsWith('archive:')) {
|
|
799
|
-
var
|
|
800
|
-
|
|
883
|
+
var archFile = noteFile.slice(8);
|
|
884
|
+
var archLabel = archFile.replace(/\.md$/, '').replace(/^\d{4}-\d{2}-\d{2}-/, '').slice(0, 30) + ' (archived)';
|
|
885
|
+
artPills += renderArtifactLink({ type: 'note', id: archFile, label: archLabel, icon: '📄', title: 'Archived note: ' + archFile }) + ' ';
|
|
801
886
|
} else {
|
|
802
887
|
var noteLabel = noteFile.replace(/\.md$/, '').slice(0, 30);
|
|
803
|
-
artPills +=
|
|
888
|
+
artPills += renderArtifactLink({ type: 'note', id: noteFile, label: noteLabel, title: 'Note: ' + noteFile }) + ' ';
|
|
804
889
|
}
|
|
805
890
|
});
|
|
806
891
|
}
|
|
807
892
|
if (artPills) html += field('Artifacts', '<div style="display:flex;flex-wrap:wrap;gap:4px">' + artPills + '</div>');
|
|
808
893
|
|
|
894
|
+
// P-34fa5d79 — Mentions: notes whose YAML frontmatter cites this WI
|
|
895
|
+
// (work_item: / wi: / sourceItem:). Slim string[] field set by
|
|
896
|
+
// engine/queries.js#getWorkItems from _buildNotesByWiMap. Each entry is
|
|
897
|
+
// either a bare inbox filename or `archive:<filename>` for archive notes.
|
|
898
|
+
if (Array.isArray(item._notes) && item._notes.length > 0) {
|
|
899
|
+
var mentionPills = item._notes.map(function(token) {
|
|
900
|
+
var isArchive = token.indexOf('archive:') === 0;
|
|
901
|
+
var fname = isArchive ? token.slice(8) : token;
|
|
902
|
+
var label = fname.replace(/\.md$/, '').slice(0, 30) + (isArchive ? ' (archived)' : '');
|
|
903
|
+
return renderArtifactLink({ type: 'note', id: fname, label: label, title: 'Note: ' + fname });
|
|
904
|
+
}).join(' ');
|
|
905
|
+
html += field('Mentions', '<div style="display:flex;flex-wrap:wrap;gap:4px">' + mentionPills + '</div>');
|
|
906
|
+
}
|
|
907
|
+
|
|
809
908
|
if (item._totalCostUsd != null) html += field('Cumulative Cost', '$' + Number(item._totalCostUsd).toFixed(4));
|
|
810
909
|
if (item._totalInputTokens) html += field('Total Input Tokens', Number(item._totalInputTokens).toLocaleString());
|
|
811
910
|
if (item._totalOutputTokens) html += field('Total Output Tokens', Number(item._totalOutputTokens).toLocaleString());
|
|
@@ -856,8 +955,14 @@ function openWorkItemDetail(id) {
|
|
|
856
955
|
merged.description = full.description || cached.description || '';
|
|
857
956
|
if (Array.isArray(full.acceptanceCriteria)) merged.acceptanceCriteria = full.acceptanceCriteria;
|
|
858
957
|
if (Array.isArray(full.references)) merged.references = full.references;
|
|
859
|
-
//
|
|
860
|
-
|
|
958
|
+
// P-ce1e5e47 — withTopFrame skips the re-render when the user has
|
|
959
|
+
// stacked another modal on top of this work-item view.
|
|
960
|
+
var applyHydration = function() {
|
|
961
|
+
// eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escapeHtml() or renderMd() by _wiRenderDetail() (fields: title, description, agent, source, reasons, references, artifacts, PR links)
|
|
962
|
+
document.getElementById('modal-body').innerHTML = _wiRenderDetail(merged);
|
|
963
|
+
};
|
|
964
|
+
if (typeof withTopFrame === 'function') withTopFrame('wi', id, applyHydration);
|
|
965
|
+
else applyHydration();
|
|
861
966
|
})
|
|
862
967
|
.catch(function() {
|
|
863
968
|
var desc = document.getElementById('wi-detail-desc');
|
|
@@ -895,7 +1000,17 @@ function viewAgentOutput(logPath) {
|
|
|
895
1000
|
|
|
896
1001
|
function openInboxNote(filename) {
|
|
897
1002
|
var idx = (inboxData || []).findIndex(function(item) { return item.name === filename; });
|
|
898
|
-
if (idx >= 0) {
|
|
1003
|
+
if (idx >= 0) {
|
|
1004
|
+
openModal(idx);
|
|
1005
|
+
// P-34fa5d79 — Source-WI chip for live inbox notes. openModal() above
|
|
1006
|
+
// paints the modal body from inboxData[idx].content; we parse the
|
|
1007
|
+
// frontmatter here and inject a back-link chip when wi: / work_item: /
|
|
1008
|
+
// sourceItem: is present. Append to body so the existing "Add to KB"
|
|
1009
|
+
// button + rendered content stay intact.
|
|
1010
|
+
var live = inboxData[idx];
|
|
1011
|
+
if (live) _wiInjectSourceChipFromFrontmatter(live.content, 'modal-body');
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
899
1014
|
// Not in the live inbox — it's an archived note (the common case for a
|
|
900
1015
|
// completed work item's note pill, which the rewriter re-encodes as an
|
|
901
1016
|
// 'archive:<name>' token). Fetch it from notes/archive/ and show it in the
|
|
@@ -911,6 +1026,9 @@ function openInboxNote(filename) {
|
|
|
911
1026
|
// eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes note content before assembling HTML; filename is set via textContent
|
|
912
1027
|
bodyEl.innerHTML = '<div style="font-size:var(--text-md);line-height:1.7;color:var(--muted)">' + renderMd(content) + '</div>';
|
|
913
1028
|
document.getElementById('modal').classList.add('open');
|
|
1029
|
+
// P-34fa5d79 — Source-WI chip for archive notes. Same convention as the
|
|
1030
|
+
// live-inbox branch above; parsed off the freshly-fetched content.
|
|
1031
|
+
_wiInjectSourceChipFromFrontmatter(content, 'modal-body');
|
|
914
1032
|
})
|
|
915
1033
|
.catch(function() {
|
|
916
1034
|
closeModal();
|
|
@@ -918,6 +1036,29 @@ function openInboxNote(filename) {
|
|
|
918
1036
|
});
|
|
919
1037
|
}
|
|
920
1038
|
|
|
1039
|
+
// P-34fa5d79 — shared frontmatter → source-WI chip injector. Parses the YAML
|
|
1040
|
+
// frontmatter block at the start of `content`, looks for the WI fields the
|
|
1041
|
+
// repo uses (work_item / wi / sourceItem), and prepends a renderArtifactLink
|
|
1042
|
+
// chip to the named modal body container so the operator can click back to
|
|
1043
|
+
// the WI that the note cites. No-op when no frontmatter / no WI field.
|
|
1044
|
+
function _wiInjectSourceChipFromFrontmatter(content, containerId) {
|
|
1045
|
+
var bodyEl = document.getElementById(containerId);
|
|
1046
|
+
if (!bodyEl) return;
|
|
1047
|
+
var m = String(content || '').match(/^---\n([\s\S]*?)\n---/);
|
|
1048
|
+
if (!m) return;
|
|
1049
|
+
var fm = {};
|
|
1050
|
+
m[1].split('\n').forEach(function(line) {
|
|
1051
|
+
var lm = line.match(/^([\w-]+):\s*(.*)$/);
|
|
1052
|
+
if (lm) fm[lm[1]] = lm[2].trim();
|
|
1053
|
+
});
|
|
1054
|
+
var wiId = fm.work_item || fm.wi || fm.sourceItem;
|
|
1055
|
+
if (!wiId) return;
|
|
1056
|
+
var chip = renderArtifactLink({ type: 'wi', id: wiId, label: wiId, title: 'Source work item: ' + wiId });
|
|
1057
|
+
var sourceBlock = '<div style="margin-bottom:10px;padding:6px 10px;background:var(--surface2);border:1px solid var(--border);border-radius:var(--radius-sm)"><span style="color:var(--muted);font-size:var(--text-sm);text-transform:uppercase;letter-spacing:0.5px;margin-right:8px">Source</span>' + chip + '</div>';
|
|
1058
|
+
// eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; chip HTML produced by renderArtifactLink() which escapes label + id via escapeHtml
|
|
1059
|
+
bodyEl.insertAdjacentHTML('afterbegin', sourceBlock);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
921
1062
|
if (typeof window !== 'undefined') {
|
|
922
1063
|
window.MinionsWork = { wiRow, renderWorkItems, editWorkItem, submitWorkItemEdit, deleteWorkItem, archiveWorkItem, toggleWorkItemArchive, retryWorkItem, wiPrev, wiNext, feedbackWorkItem, submitFeedback, openCreateWorkItemModal, openWorkItemDetail, openAllWorkItems, viewAgentOutput, openInboxNote, needsAttentionInfo };
|
|
923
1064
|
}
|
package/dashboard/js/state.js
CHANGED
|
@@ -70,6 +70,10 @@ function _invokePageHooks(names) {
|
|
|
70
70
|
|
|
71
71
|
function switchPage(page, pushState) {
|
|
72
72
|
_invokePageHooks(PAGE_LEAVE_HOOKS);
|
|
73
|
+
// P-ce1e5e47 — page change clears the modal stack and closes any open modal.
|
|
74
|
+
// Per-modal polls observe withTopFrame and self-stop when the frame goes away.
|
|
75
|
+
if (typeof resetModalStack === 'function') resetModalStack();
|
|
76
|
+
if (typeof _physicallyCloseModal === 'function') _physicallyCloseModal();
|
|
73
77
|
|
|
74
78
|
currentPage = page;
|
|
75
79
|
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
@@ -109,8 +113,113 @@ function switchPage(page, pushState) {
|
|
|
109
113
|
|
|
110
114
|
// Browser back/forward navigation
|
|
111
115
|
window.addEventListener('popstate', (e) => {
|
|
112
|
-
|
|
116
|
+
// P-ce1e5e47 — modal-stack-aware popstate. State shape (per history.pushState
|
|
117
|
+
// in pushModalFrame / pushModalBack):
|
|
118
|
+
// { page: 'home', modal: { breadcrumbs: [{type,id}, ...], current: {type,id} | null } }
|
|
119
|
+
// The "page" key takes precedence: a real page navigation (sidebar / browser
|
|
120
|
+
// back across tabs) clears the modal stack entirely.
|
|
121
|
+
const newPage = e.state?.page || getPageFromUrl();
|
|
122
|
+
if (newPage !== currentPage) {
|
|
123
|
+
// Page change wipes any open modal stack.
|
|
124
|
+
if (typeof resetModalStack === 'function') resetModalStack();
|
|
125
|
+
if (typeof _physicallyCloseModal === 'function') _physicallyCloseModal();
|
|
126
|
+
switchPage(newPage, false);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Same page — diff the modal stack and pop frames to match. We compute the
|
|
130
|
+
// desired total depth from breadcrumbs.length + (current ? 1 : 0).
|
|
131
|
+
if (typeof _popOneFrameInternal !== 'function' || typeof _modalStackDepth !== 'function') return;
|
|
132
|
+
const desired = e.state?.modal;
|
|
133
|
+
const desiredDepth = desired
|
|
134
|
+
? (Array.isArray(desired.breadcrumbs) ? desired.breadcrumbs.length : 0) + (desired.current ? 1 : 0)
|
|
135
|
+
: 0;
|
|
136
|
+
const currentDepth = _modalStackDepth();
|
|
137
|
+
if (currentDepth <= desiredDepth) {
|
|
138
|
+
// No frames to pop. Refresh chrome and bail.
|
|
139
|
+
if (typeof _updateModalChrome === 'function') _updateModalChrome();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
_suppressHistoryFromPopstate = true;
|
|
143
|
+
try {
|
|
144
|
+
let safety = 0;
|
|
145
|
+
while (_modalStackDepth() > desiredDepth && safety++ < 32) {
|
|
146
|
+
_popOneFrameInternal();
|
|
147
|
+
}
|
|
148
|
+
if (typeof _updateModalChrome === 'function') _updateModalChrome();
|
|
149
|
+
} finally {
|
|
150
|
+
_suppressHistoryFromPopstate = false;
|
|
151
|
+
}
|
|
113
152
|
});
|
|
153
|
+
|
|
154
|
+
// P-ce1e5e47 — parse #modal=type:id,type:id,… and return an array of
|
|
155
|
+
// { type, id } frames in bottom→top order. Used both for replay on initial
|
|
156
|
+
// page load and for unit tests of the URL contract.
|
|
157
|
+
// P-3ed68b1e — separator is ',' (was '>'); '>' is in the WHATWG URL
|
|
158
|
+
// fragment percent-encode set and Chromium serialises it as '%3E' on
|
|
159
|
+
// history.pushState — that broke split('>') for every multi-frame hash.
|
|
160
|
+
// We also still accept the legacy '>' / '%3E' separators so any bookmark
|
|
161
|
+
// or in-flight URL from before the fix continues to replay correctly.
|
|
162
|
+
function getModalStackFromUrl() {
|
|
163
|
+
const hash = window.location.hash || '';
|
|
164
|
+
const idx = hash.indexOf('modal=');
|
|
165
|
+
if (idx < 0) return [];
|
|
166
|
+
const raw = hash.slice(idx + 'modal='.length);
|
|
167
|
+
if (!raw) return [];
|
|
168
|
+
const out = [];
|
|
169
|
+
const parts = raw.split(/,|%3E|>/i);
|
|
170
|
+
for (let i = 0; i < parts.length; i++) {
|
|
171
|
+
const seg = parts[i];
|
|
172
|
+
if (!seg) continue;
|
|
173
|
+
// type:id — id may contain encoded ':' so split on the first ':' only.
|
|
174
|
+
const colon = seg.indexOf(':');
|
|
175
|
+
if (colon < 0) continue;
|
|
176
|
+
let type, id;
|
|
177
|
+
try {
|
|
178
|
+
type = decodeURIComponent(seg.slice(0, colon));
|
|
179
|
+
id = decodeURIComponent(seg.slice(colon + 1));
|
|
180
|
+
} catch { continue; }
|
|
181
|
+
if (!type || !id) continue;
|
|
182
|
+
out.push({ type, id });
|
|
183
|
+
}
|
|
184
|
+
// Cap at MAX (defined in utils.js); take the last N.
|
|
185
|
+
const cap = (typeof MODAL_STACK_MAX_DEPTH === 'number') ? MODAL_STACK_MAX_DEPTH : 5;
|
|
186
|
+
return out.length > cap ? out.slice(-cap) : out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// P-ce1e5e47 — replay the modal stack encoded in the URL hash on initial load.
|
|
190
|
+
// Uses history.replaceState (via pushModalFrame's history call only if we
|
|
191
|
+
// skip it) — we want a single history entry after replay, not N. We achieve
|
|
192
|
+
// this by suppressing pushState for the first N-1 pushes and letting the
|
|
193
|
+
// final push collapse into a replaceState.
|
|
194
|
+
function replayModalStackFromUrl() {
|
|
195
|
+
const frames = (typeof getModalStackFromUrl === 'function') ? getModalStackFromUrl() : [];
|
|
196
|
+
if (!frames.length) return;
|
|
197
|
+
if (typeof openArtifact !== 'function') return;
|
|
198
|
+
// Suppress per-push pushState during replay; afterward write a single
|
|
199
|
+
// replaceState that captures the assembled stack.
|
|
200
|
+
if (typeof _suppressHistoryFromPopstate !== 'undefined') _suppressHistoryFromPopstate = true;
|
|
201
|
+
try {
|
|
202
|
+
for (let i = 0; i < frames.length; i++) {
|
|
203
|
+
try { openArtifact(frames[i].type, frames[i].id); } catch {}
|
|
204
|
+
}
|
|
205
|
+
} finally {
|
|
206
|
+
if (typeof _suppressHistoryFromPopstate !== 'undefined') _suppressHistoryFromPopstate = false;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const state = { page: currentPage, modal: _serializeStackForHistory() };
|
|
210
|
+
history.replaceState(state, '', _buildModalHash());
|
|
211
|
+
} catch {}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Kick off replay once the DOM (and all renderer modules) is in place.
|
|
215
|
+
if (typeof document !== 'undefined') {
|
|
216
|
+
if (document.readyState === 'loading') {
|
|
217
|
+
document.addEventListener('DOMContentLoaded', function() { try { replayModalStackFromUrl(); } catch {} });
|
|
218
|
+
} else if (typeof setTimeout !== 'undefined') {
|
|
219
|
+
// Defer to next tick so all openXxxDetail functions are registered first.
|
|
220
|
+
setTimeout(function() { try { replayModalStackFromUrl(); } catch {} }, 0);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
114
223
|
window._prdRequeueUi = window._prdRequeueUi || {};
|
|
115
224
|
|
|
116
225
|
function getPrdRequeueState(workItemId) {
|