@yemi33/minions 0.1.2194 → 0.1.2196

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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="btn-destructive" style="padding:1px 5px" data-pr-id="' + escapeHtml(String(prId)) + '" onclick="event.stopPropagation();unlinkPr(this.dataset.prId)" title="Remove from tracking">x</button></td>' +
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)">&#x2197;</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:50px">' + // Actions
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 &#x2197;</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
 
@@ -575,4 +575,154 @@ function restoreDashboardScrollState(root, state) {
575
575
  });
576
576
  }
577
577
 
578
- window.MinionsRenderUtils = { formatToolSummary, renderAgentOutput, renderTerminalBanner, renderPager, pinButton, captureDashboardScrollState, restoreDashboardScrollState };
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> ' + escHtml(_formatWatchTarget(w.target, w.targetType)) + '</div>' +
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 ? ' (dispatched: ' + escHtml(w._lastActionResult.dispatchedItemId) + ')' : '') + '</div>' : '') +
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
- // eslint-disable-next-line no-unsanitized/property -- reason: _renderWatchHistoryDetail() escapes all user-controlled fields before assembling HTML
393
- container.innerHTML = _renderWatchHistoryDetail(data && data.history);
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
- // Fall back to the inline _history from /api/watches (best-effort).
397
- // eslint-disable-next-line no-unsanitized/property -- reason: _renderWatchHistoryDetail() escapes all user-controlled fields before assembling HTML
398
- container.innerHTML = _renderWatchHistoryDetail(w._history || []);
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
  };