@yemi33/minions 0.1.2048 → 0.1.2050

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.
Files changed (36) hide show
  1. package/dashboard/js/command-center.js +72 -0
  2. package/dashboard/js/command-history.js +2 -1
  3. package/dashboard/js/command-input.js +7 -3
  4. package/dashboard/js/command-parser.js +1 -0
  5. package/dashboard/js/detail-panel.js +7 -0
  6. package/dashboard/js/fre.js +1 -0
  7. package/dashboard/js/live-stream.js +2 -0
  8. package/dashboard/js/modal-qa.js +9 -0
  9. package/dashboard/js/modal.js +5 -0
  10. package/dashboard/js/qa.js +6 -0
  11. package/dashboard/js/refresh.js +8 -1
  12. package/dashboard/js/render-agents.js +30 -3
  13. package/dashboard/js/render-dispatch.js +12 -0
  14. package/dashboard/js/render-inbox.js +6 -0
  15. package/dashboard/js/render-kb.js +4 -0
  16. package/dashboard/js/render-managed.js +1 -0
  17. package/dashboard/js/render-meetings.js +6 -0
  18. package/dashboard/js/render-other.js +11 -0
  19. package/dashboard/js/render-pinned.js +2 -0
  20. package/dashboard/js/render-pipelines.js +5 -0
  21. package/dashboard/js/render-plans.js +9 -0
  22. package/dashboard/js/render-prd.js +8 -0
  23. package/dashboard/js/render-prs.js +4 -0
  24. package/dashboard/js/render-schedules.js +5 -0
  25. package/dashboard/js/render-skills.js +2 -0
  26. package/dashboard/js/render-watches.js +10 -0
  27. package/dashboard/js/render-work-items.js +9 -0
  28. package/dashboard/js/settings.js +387 -188
  29. package/dashboard/slim.html +8 -8
  30. package/dashboard/styles.css +35 -0
  31. package/docs/deprecated.json +57 -8
  32. package/engine/lifecycle.js +0 -1
  33. package/engine/queries.js +37 -5
  34. package/package.json +6 -3
  35. package/playbooks/fix.md +15 -5
  36. package/playbooks/shared-rules.md +29 -0
@@ -81,6 +81,7 @@ function _ccStripActionBlockFromText(value) {
81
81
  var firstUser = legacyMessages.find(function(m) { return m.role === 'user'; });
82
82
  if (firstUser) {
83
83
  var tmp = document.createElement('div');
84
+ // eslint-disable-next-line no-unsanitized/property -- reason: renderMd()/escHtml() produced the persisted user message HTML before this title extraction (see dashboard/js/utils.js)
84
85
  tmp.innerHTML = firstUser.html;
85
86
  var txt = (tmp.textContent || tmp.innerText || '').trim();
86
87
  if (txt.length > 0) title = txt.slice(0, CC_TITLE_MAX_LENGTH);
@@ -115,6 +116,7 @@ function _ccBuildTranscript(tab) {
115
116
  if (!m || (m.role !== 'user' && m.role !== 'assistant' && m.role !== 'action' && m.role !== 'system')) continue;
116
117
  var html = typeof m.html === 'string' ? m.html : '';
117
118
  var tmp = document.createElement('div');
119
+ // eslint-disable-next-line no-unsanitized/property -- reason: renderMd()/escHtml() produced persisted message HTML before this transcript text extraction (see dashboard/js/utils.js)
118
120
  tmp.innerHTML = html;
119
121
  var text = (tmp.textContent || tmp.innerText || '').trim();
120
122
  if (text) out.push({ role: m.role, text: text });
@@ -566,6 +568,7 @@ function ccSwitchTab(id) {
566
568
  var restoreInterval = setInterval(function() {
567
569
  var re = document.getElementById('cc-restore-thinking');
568
570
  if (!re || !tab._sending || _ccActiveTabId !== tab.id) { clearInterval(restoreInterval); return; }
571
+ // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() and renderToolChip() escape user-controlled stream/tool fields before assembling restored stream HTML
569
572
  re.innerHTML = _restoreStreamHtml();
570
573
  if (el.scrollHeight - el.scrollTop - el.clientHeight < 150) el.scrollTop = el.scrollHeight;
571
574
  }, 1000);
@@ -644,6 +647,7 @@ function ccRenderTabBar() {
644
647
  }
645
648
  html += '<div class="cc-tab cc-tab-new" draggable="false" ondragstart="event.preventDefault();event.stopPropagation();" onclick="ccNewTab()" title="New tab">+</div>';
646
649
  html += '</div>';
650
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; tab titles are wrapped in escHtml() and tab ids are generated internally (fields: t.title)
647
651
  bar.innerHTML = html;
648
652
  }
649
653
 
@@ -739,6 +743,7 @@ function ccRestoreMessages() {
739
743
  var thinking = document.createElement('div');
740
744
  thinking.id = 'cc-thinking';
741
745
  thinking.style.cssText = 'padding:8px 12px;border-radius:8px;font-size:11px;color:var(--muted);align-self:flex-start;display:flex;align-items:center;gap:8px';
746
+ // eslint-disable-next-line no-unsanitized/property -- reason: composed from elapsed timer and compile-time thinking UI strings (no user data flows in)
742
747
  thinking.innerHTML = '<span class="dot-pulse" style="display:inline-flex;gap:3px"><span style="width:4px;height:4px;background:var(--blue);border-radius:50%;animation:dotPulse 1.2s infinite"></span><span style="width:4px;height:4px;background:var(--blue);border-radius:50%;animation:dotPulse 1.2s infinite;animation-delay:0.2s"></span><span style="width:4px;height:4px;background:var(--blue);border-radius:50%;animation:dotPulse 1.2s infinite;animation-delay:0.4s"></span></span> <span id="cc-thinking-text">Still working...</span> <span id="cc-thinking-time" style="font-size:10px;color:var(--border)">' + Math.floor(elapsed / 1000) + 's</span>' +
743
748
  ' <button onclick="ccNewTab()" style="font-size:9px;padding:2px 8px;background:var(--surface2);border:1px solid var(--border);border-radius:4px;color:var(--red);cursor:pointer">Reset</button>';
744
749
  el.appendChild(thinking);
@@ -828,6 +833,7 @@ function ccAddMessage(role, html, skipSave, targetTabId, meta) {
828
833
  if (meta && meta.retryId) div.setAttribute('data-cc-retry-id', meta.retryId);
829
834
  div.style.cssText = 'padding:8px 12px;border-radius:8px;font-size:12px;line-height:1.6;max-width:95%;' +
830
835
  (isUser ? 'background:var(--blue);color:#fff;align-self:flex-end' : isSystem ? 'align-self:center;max-width:100%' : isAction ? 'align-self:flex-start;padding:2px 0' : 'background:var(--surface2);color:var(--text);align-self:flex-start;border:1px solid var(--border);position:relative');
836
+ // eslint-disable-next-line no-unsanitized/property -- reason: renderMd()/escHtml() call sites escape user-controlled message HTML before ccAddMessage() assigns it (see dashboard/js/utils.js)
831
837
  div.innerHTML = (isAssistant && !html.includes('color:var(--red)') && !html.includes('cc-queued-pill') ? llmCopyBtn() : '') + html;
832
838
  var wasNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 150;
833
839
  el.appendChild(div);
@@ -845,6 +851,7 @@ function ccAddMessage(role, html, skipSave, targetTabId, meta) {
845
851
  // current topic, not whatever the user happened to type first.
846
852
  if (role === 'user') {
847
853
  var tmp = document.createElement('div');
854
+ // eslint-disable-next-line no-unsanitized/property -- reason: renderMd()/escHtml() produced the saved user message HTML before this title extraction (see dashboard/js/utils.js)
848
855
  tmp.innerHTML = html;
849
856
  var txt = (tmp.textContent || tmp.innerText || '').trim();
850
857
  if (txt.length > 0) {
@@ -902,6 +909,7 @@ function _renderQueueIndicator() {
902
909
  var el = document.createElement('div');
903
910
  el.className = 'cc-queue-item';
904
911
  el.style.cssText = 'padding:8px 12px;border-radius:8px;font-size:12px;line-height:1.6;max-width:95%;align-self:flex-end;background:var(--blue);color:#fff;opacity:0.5;order:9999';
912
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; queued message text wrapped in escHtml() (fields: m.message)
905
913
  el.innerHTML = escHtml(typeof m === 'string' ? m : m.message) + '<div style="font-size:9px;opacity:0.7;font-style:italic;margin-top:2px">queued</div>';
906
914
  msgs.appendChild(el);
907
915
  });
@@ -1018,6 +1026,7 @@ async function _ccDoSend(message, skipUserMsg, forceTabId, intentMetadata) {
1018
1026
  html += '<div style="margin-top:6px;font-size:10px;color:var(--muted)">' + escHtml(streamStatusNote) + '</div>';
1019
1027
  }
1020
1028
  html += '<div style="margin-top:' + (streamedText ? '6px' : '0') + '">' + _getThinkingHtml() + '</div>';
1029
+ // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() and renderToolChip() escape streamed text/tool fields before assembling live stream HTML
1021
1030
  streamDiv.innerHTML = html;
1022
1031
  // Re-append queue indicators so they stay below the streaming content
1023
1032
  if (activeTab._queue && activeTab._queue.length > 0) _renderQueueIndicator();
@@ -1285,6 +1294,7 @@ function ccRetryLast(tabId, retryId) {
1285
1294
  if (!last) return;
1286
1295
  // Backward-compatible fallback for retry buttons rendered before retry context existed.
1287
1296
  var tmp = document.createElement('div');
1297
+ // eslint-disable-next-line no-unsanitized/property -- reason: renderMd()/escHtml() produced the saved retry message HTML before this text extraction (see dashboard/js/utils.js)
1288
1298
  tmp.innerHTML = last.html;
1289
1299
  text = tmp.textContent || tmp.innerText || '';
1290
1300
  }
@@ -1377,6 +1387,7 @@ function _ccAppendHtmlToMessage(tabOrId, messageId, html) {
1377
1387
  msg.html = (msg.html || '') + html;
1378
1388
  if (typeof _ccActiveTabId !== 'undefined' && tab.id === _ccActiveTabId && typeof document !== 'undefined') {
1379
1389
  var el = document.getElementById(_ccMessageDomId(messageId));
1390
+ // eslint-disable-next-line no-unsanitized/method -- reason: structural HTML is a string literal; appended action status fragments wrap user fields in escHtml() before status.outerHTML is passed here
1380
1391
  if (el && typeof el.insertAdjacentHTML === 'function') el.insertAdjacentHTML('beforeend', html);
1381
1392
  }
1382
1393
  return true;
@@ -1418,12 +1429,14 @@ async function ccExecuteAction(action, targetTabId, opts) {
1418
1429
  if (action._serverExecuted) {
1419
1430
  if (action._serverHidden) return;
1420
1431
  if (action._serverError) {
1432
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; server action type and error wrapped in escHtml() (fields: action.type, action._serverError)
1421
1433
  status.innerHTML = '&#10007; ' + escHtml(action.type) + ' failed: ' + escHtml(action._serverError);
1422
1434
  status.style.color = 'var(--red)';
1423
1435
  } else {
1424
1436
  var label = action._serverId ? escHtml(action._serverId) : escHtml(action.title || action.type);
1425
1437
  var serverActionType = action.type || '';
1426
1438
  var successLabel = serverActionType === 'dispatch' ? 'Dispatched' : serverActionType;
1439
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; label and warning wrapped in escHtml(), success label is a normalized server action type (fields: label, action._serverWarning)
1427
1440
  status.innerHTML = '&#10003; ' + escHtml(successLabel) + ': <strong>' + label + '</strong>' +
1428
1441
  (action._serverDuplicate ? '<div style="font-size:10px;color:var(--orange);margin-top:2px">Already existed from a previous request; no duplicate work item was created.</div>' : '') +
1429
1442
  (action._serverWarning ? '<div style="font-size:10px;color:var(--muted);margin-top:2px">' + escHtml(action._serverWarning) + '</div>' : '');
@@ -1461,6 +1474,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1461
1474
  agents: action.agents || [],
1462
1475
  });
1463
1476
  var d = await res.json();
1477
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; dispatch id/title wrapped in escHtml() (fields: d.id, action.title)
1464
1478
  status.innerHTML = '&#10003; Dispatched: <strong>' + escHtml(d.id || action.title) + '</strong>';
1465
1479
  status.style.color = 'var(--green)';
1466
1480
  wakeEngine();
@@ -1468,6 +1482,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1468
1482
  }
1469
1483
  case 'note': {
1470
1484
  await _ccFetch('/api/notes', { title: action.title, what: action.content || action.description, author: 'command-center' });
1485
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; note title wrapped in escHtml() (fields: action.title)
1471
1486
  status.innerHTML = '&#10003; Note saved: <strong>' + escHtml(action.title) + '</strong>';
1472
1487
  status.style.color = 'var(--green)';
1473
1488
  var notePageLink = document.querySelector('.sidebar-link[data-page="inbox"]');
@@ -1477,6 +1492,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1477
1492
  case 'pin':
1478
1493
  case 'pin-to-pinned': {
1479
1494
  await _ccFetch('/api/pinned', { title: action.title, content: action.content || action.description, level: action.level || '' });
1495
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; pinned note title wrapped in escHtml() (fields: action.title)
1480
1496
  status.innerHTML = '&#x1F4CC; Pinned: <strong>' + escHtml(action.title) + '</strong> — visible to all agents';
1481
1497
  status.style.color = 'var(--green)';
1482
1498
  break;
@@ -1484,6 +1500,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1484
1500
  case 'plan': {
1485
1501
  var branchStrategy = action.branch_strategy || action.branchStrategy || 'parallel';
1486
1502
  await _ccFetch('/api/plan', { title: action.title, description: action.description, project: action.project, branch_strategy: branchStrategy, branchStrategy: branchStrategy });
1503
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; plan title wrapped in escHtml() (fields: action.title)
1487
1504
  status.innerHTML = '&#10003; Plan queued: <strong>' + escHtml(action.title) + '</strong>';
1488
1505
  status.style.color = 'var(--green)';
1489
1506
  wakeEngine();
@@ -1492,6 +1509,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1492
1509
  case 'cancel': {
1493
1510
  var cancelAgent = action.agent || action.agentId || '';
1494
1511
  await _ccFetch('/api/agents/cancel', { agent: cancelAgent, agentId: cancelAgent, task: action.task || action.cancelTask || '', reason: action.reason || 'Cancelled via command center' });
1512
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; cancel target wrapped in escHtml() (fields: cancelAgent, action.task, action.cancelTask)
1495
1513
  status.innerHTML = '&#10003; Cancelled agent: <strong>' + escHtml(cancelAgent || action.task || action.cancelTask || '') + '</strong>';
1496
1514
  status.style.color = 'var(--orange)';
1497
1515
  break;
@@ -1500,6 +1518,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1500
1518
  for (var ri = 0; ri < (action.ids || []).length; ri++) {
1501
1519
  await _ccFetch('/api/work-items/retry', { id: action.ids[ri], source: '' });
1502
1520
  }
1521
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; retried ids wrapped in escHtml() (fields: action.ids)
1503
1522
  status.innerHTML = '&#10003; Retried: <strong>' + escHtml((action.ids || []).join(', ')) + '</strong>';
1504
1523
  status.style.color = 'var(--green)';
1505
1524
  wakeEngine();
@@ -1507,12 +1526,14 @@ async function ccExecuteAction(action, targetTabId, opts) {
1507
1526
  }
1508
1527
  case 'pause-plan': {
1509
1528
  await _ccFetch('/api/plans/pause', { file: action.file });
1529
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; plan file wrapped in escHtml() (fields: action.file)
1510
1530
  status.innerHTML = '&#10003; Paused plan: <strong>' + escHtml(action.file) + '</strong>';
1511
1531
  status.style.color = 'var(--orange)';
1512
1532
  break;
1513
1533
  }
1514
1534
  case 'approve-plan': {
1515
1535
  await _ccFetch('/api/plans/approve', { file: action.file });
1536
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; plan file wrapped in escHtml() (fields: action.file)
1516
1537
  status.innerHTML = '&#10003; Approved plan: <strong>' + escHtml(action.file) + '</strong>';
1517
1538
  status.style.color = 'var(--green)';
1518
1539
  wakeEngine();
@@ -1534,6 +1555,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1534
1555
  }
1535
1556
  await _ccFetch('/api/plans/approve', { file: action.file });
1536
1557
  var resumeCount = (action.updates || []).length + (action.newItems || []).length;
1558
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; plan file wrapped in escHtml(), item count is numeric (fields: action.file)
1537
1559
  status.innerHTML = '&#10003; Resumed plan: <strong>' + escHtml(action.file) + '</strong>' + (resumeCount > 0 ? ' (' + resumeCount + ' item(s) updated)' : '');
1538
1560
  status.style.color = 'var(--green)';
1539
1561
  wakeEngine();
@@ -1541,24 +1563,28 @@ async function ccExecuteAction(action, targetTabId, opts) {
1541
1563
  }
1542
1564
  case 'edit-prd-item': {
1543
1565
  await _ccFetch('/api/prd-items/update', { source: action.source, itemId: action.itemId, name: action.name, description: action.description, priority: action.priority, estimated_complexity: action.complexity });
1566
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; PRD item id wrapped in escHtml() (fields: action.itemId)
1544
1567
  status.innerHTML = '&#10003; Updated PRD item: <strong>' + escHtml(action.itemId) + '</strong>';
1545
1568
  status.style.color = 'var(--green)';
1546
1569
  break;
1547
1570
  }
1548
1571
  case 'remove-prd-item': {
1549
1572
  await _ccFetch('/api/prd-items/remove', { source: action.source, itemId: action.itemId });
1573
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; PRD item id wrapped in escHtml() (fields: action.itemId)
1550
1574
  status.innerHTML = '&#10003; Removed PRD item: <strong>' + escHtml(action.itemId) + '</strong>';
1551
1575
  status.style.color = 'var(--orange)';
1552
1576
  break;
1553
1577
  }
1554
1578
  case 'delete-work-item': {
1555
1579
  await _ccFetch('/api/work-items/delete', { id: action.id, source: action.source || '' });
1580
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; work item id wrapped in escHtml() (fields: action.id)
1556
1581
  status.innerHTML = '&#10003; Deleted work item: <strong>' + escHtml(action.id) + '</strong>';
1557
1582
  status.style.color = 'var(--orange)';
1558
1583
  break;
1559
1584
  }
1560
1585
  case 'cancel-work-item': {
1561
1586
  await _ccFetch('/api/work-items/cancel', { id: action.id, source: action.source || '', reason: action.reason || 'cc' });
1587
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; work item id wrapped in escHtml() (fields: action.id)
1562
1588
  status.innerHTML = '&#10003; Cancelled work item: <strong>' + escHtml(action.id) + '</strong>';
1563
1589
  status.style.color = 'var(--orange)';
1564
1590
  wakeEngine();
@@ -1579,9 +1605,11 @@ async function ccExecuteAction(action, targetTabId, opts) {
1579
1605
  });
1580
1606
  var data = await res2.json();
1581
1607
  if (data.ok && data.edited) {
1608
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; plan file wrapped in escHtml() (fields: action.file)
1582
1609
  status.innerHTML = '&#10003; Plan edited: <strong>' + escHtml(action.file) + '</strong>';
1583
1610
  status.style.color = 'var(--green)';
1584
1611
  } else {
1612
+ // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes doc-chat answer text before assembling HTML (see dashboard/js/utils.js)
1585
1613
  status.innerHTML = data.answer ? renderMd(data.answer) : '&#10007; Could not edit plan';
1586
1614
  status.style.color = data.answer ? 'var(--muted)' : 'var(--red)';
1587
1615
  }
@@ -1589,6 +1617,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1589
1617
  }
1590
1618
  case 'execute-plan': {
1591
1619
  await _ccFetch('/api/plans/execute', { file: action.file, project: action.project || '' });
1620
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; plan file wrapped in escHtml() (fields: action.file)
1592
1621
  status.innerHTML = '&#10003; Plan execution queued: <strong>' + escHtml(action.file) + '</strong>';
1593
1622
  status.style.color = 'var(--green)';
1594
1623
  wakeEngine();
@@ -1607,9 +1636,11 @@ async function ccExecuteAction(action, targetTabId, opts) {
1607
1636
  });
1608
1637
  var data3 = await res3.json();
1609
1638
  if (data3.ok && data3.edited) {
1639
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; file path wrapped in escHtml() (fields: action.file)
1610
1640
  status.innerHTML = '&#10003; Edited: <strong>' + escHtml(action.file) + '</strong>';
1611
1641
  status.style.color = 'var(--green)';
1612
1642
  } else {
1643
+ // eslint-disable-next-line no-unsanitized/property -- reason: renderMd() escapes doc-chat answer text before assembling HTML (see dashboard/js/utils.js)
1613
1644
  status.innerHTML = data3.answer ? renderMd(data3.answer) : '&#10007; Could not edit file';
1614
1645
  status.style.color = data3.answer ? 'var(--muted)' : 'var(--red)';
1615
1646
  }
@@ -1628,6 +1659,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1628
1659
  })
1629
1660
  });
1630
1661
  if (!res4.ok) { var d4 = await res4.json().catch(function() { return {}; }); throw new Error(d4.error || 'Schedule create failed'); }
1662
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; schedule id wrapped in escHtml() and action label is a fixed branch (fields: action.id)
1631
1663
  status.innerHTML = '&#10003; Schedule ' + (action._update ? 'updated' : 'created') + ': <strong>' + escHtml(action.id) + '</strong>';
1632
1664
  status.style.color = 'var(--green)';
1633
1665
  break;
@@ -1638,6 +1670,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1638
1670
  body: JSON.stringify({ id: action.id })
1639
1671
  });
1640
1672
  if (!res5.ok) { var d5 = await res5.json().catch(function() { return {}; }); throw new Error(d5.error || 'Schedule delete failed'); }
1673
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; schedule id wrapped in escHtml() (fields: action.id)
1641
1674
  status.innerHTML = '&#10003; Deleted schedule: <strong>' + escHtml(action.id) + '</strong>';
1642
1675
  status.style.color = 'var(--orange)';
1643
1676
  break;
@@ -1650,6 +1683,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1650
1683
  });
1651
1684
  if (!res6.ok) { var d6 = await res6.json().catch(function() { return {}; }); throw new Error(d6.error || 'Meeting create failed'); }
1652
1685
  var d6r = await res6.json();
1686
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; meeting title and returned id wrapped in escHtml() (fields: action.title, d6r.id)
1653
1687
  status.innerHTML = '&#10003; Meeting started: <strong>' + escHtml(action.title) + '</strong>' + (d6r.id ? ' (' + escHtml(d6r.id) + ')' : '');
1654
1688
  status.style.color = 'var(--green)';
1655
1689
  wakeEngine();
@@ -1663,6 +1697,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1663
1697
  body: JSON.stringify(payload)
1664
1698
  });
1665
1699
  if (!res7.ok) { var d7 = await res7.json().catch(function() { return {}; }); throw new Error(d7.error || 'Config update failed'); }
1700
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; setting name/value wrapped in escHtml() (fields: action.setting, action.value)
1666
1701
  status.innerHTML = '&#10003; Set <strong>' + escHtml(action.setting) + '</strong> = ' + escHtml(String(action.value));
1667
1702
  status.style.color = 'var(--green)';
1668
1703
  break;
@@ -1677,36 +1712,42 @@ async function ccExecuteAction(action, targetTabId, opts) {
1677
1712
  body: JSON.stringify(body)
1678
1713
  });
1679
1714
  if (!res8.ok) { var d8 = await res8.json().catch(function() { return {}; }); throw new Error(d8.error || 'Pipeline update failed'); }
1715
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; pipeline id wrapped in escHtml() (fields: action.id)
1680
1716
  status.innerHTML = '&#10003; Updated pipeline: <strong>' + escHtml(action.id) + '</strong>';
1681
1717
  status.style.color = 'var(--green)';
1682
1718
  break;
1683
1719
  }
1684
1720
  case 'unpin': {
1685
1721
  await _ccFetch('/api/pinned/remove', { title: action.title });
1722
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; pinned title wrapped in escHtml() (fields: action.title)
1686
1723
  status.innerHTML = '&#10003; Unpinned: <strong>' + escHtml(action.title) + '</strong>';
1687
1724
  status.style.color = 'var(--green)';
1688
1725
  break;
1689
1726
  }
1690
1727
  case 'archive-plan': {
1691
1728
  await _ccFetch('/api/plans/archive', { file: action.file });
1729
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; plan file wrapped in escHtml() (fields: action.file)
1692
1730
  status.innerHTML = '&#10003; Archived plan: <strong>' + escHtml(action.file) + '</strong>';
1693
1731
  status.style.color = 'var(--green)';
1694
1732
  break;
1695
1733
  }
1696
1734
  case 'reject-plan': {
1697
1735
  await _ccFetch('/api/plans/reject', { file: action.file, reason: action.reason || '' });
1736
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; plan file wrapped in escHtml() (fields: action.file)
1698
1737
  status.innerHTML = '&#10003; Rejected plan: <strong>' + escHtml(action.file) + '</strong>';
1699
1738
  status.style.color = 'var(--orange)';
1700
1739
  break;
1701
1740
  }
1702
1741
  case 'steer-agent': {
1703
1742
  await _ccFetch('/api/agents/steer', { agent: action.agent, message: action.message || action.content });
1743
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; agent id wrapped in escHtml() (fields: action.agent)
1704
1744
  status.innerHTML = '&#10003; Steering message sent to <strong>' + escHtml(action.agent) + '</strong>';
1705
1745
  status.style.color = 'var(--green)';
1706
1746
  break;
1707
1747
  }
1708
1748
  case 'add-meeting-note': {
1709
1749
  await _ccFetch('/api/meetings/note', { id: action.id, note: action.note || action.content });
1750
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; meeting id wrapped in escHtml() (fields: action.id)
1710
1751
  status.innerHTML = '&#10003; Note added to meeting <strong>' + escHtml(action.id) + '</strong>';
1711
1752
  status.style.color = 'var(--green)';
1712
1753
  break;
@@ -1717,6 +1758,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1717
1758
  body: JSON.stringify({ id: action.id })
1718
1759
  });
1719
1760
  if (!res9.ok) { var d9 = await res9.json().catch(function() { return {}; }); throw new Error(d9.error || 'Pipeline trigger failed'); }
1761
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; pipeline id wrapped in escHtml() (fields: action.id)
1720
1762
  status.innerHTML = '&#10003; Pipeline triggered: <strong>' + escHtml(action.id) + '</strong>';
1721
1763
  status.style.color = 'var(--green)';
1722
1764
  wakeEngine();
@@ -1725,6 +1767,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1725
1767
  case 'link-pr': {
1726
1768
  var prLinkRes = await _ccFetch('/api/pull-requests/link', { url: action.url, title: action.title || '', project: action.project || '', autoObserve: action.autoObserve !== false });
1727
1769
  var prLinkData = await prLinkRes.json().catch(function() { return {}; });
1770
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; PR URL and server message wrapped in escHtml() (fields: action.url, prLinkData.message)
1728
1771
  status.innerHTML = '&#10003; PR linked: <strong>' + escHtml(action.url) + '</strong>' +
1729
1772
  (prLinkData.message ? '<div style="font-size:11px;color:var(--muted);margin-top:4px">' + escHtml(prLinkData.message) + '</div>' : '');
1730
1773
  status.style.color = 'var(--green)';
@@ -1732,6 +1775,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1732
1775
  }
1733
1776
  case 'archive-meeting': {
1734
1777
  await _ccFetch('/api/meetings/archive', { id: action.id });
1778
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; meeting id wrapped in escHtml() (fields: action.id)
1735
1779
  status.innerHTML = '&#10003; Meeting archived: <strong>' + escHtml(action.id) + '</strong>';
1736
1780
  status.style.color = 'var(--green)';
1737
1781
  break;
@@ -1747,8 +1791,10 @@ async function ccExecuteAction(action, targetTabId, opts) {
1747
1791
  var d10 = await res10.json();
1748
1792
  var labelWarning = d10.warning ? ' <span style="color:var(--orange)">(' + escHtml(d10.warning) + ')</span>' : '';
1749
1793
  if (d10.url) {
1794
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; issue URL, title, and warning wrapped in escHtml() (fields: d10.url, action.title, d10.warning)
1750
1795
  status.innerHTML = '&#128027; Bug filed: <a href="' + escHtml(d10.url) + '" target="_blank" style="color:var(--blue)">' + escHtml(action.title) + '</a>' + labelWarning;
1751
1796
  } else {
1797
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; issue title and warning wrapped in escHtml(), fallback URL is static (fields: action.title, d10.warning)
1752
1798
  status.innerHTML = '&#128027; Bug filed: <strong>' + escHtml(action.title) + '</strong> — <a href="https://github.com/yemi33/minions/issues" target="_blank" style="color:var(--blue)">view issues</a>' + labelWarning;
1753
1799
  }
1754
1800
  status.style.color = 'var(--green)';
@@ -1757,10 +1803,12 @@ async function ccExecuteAction(action, targetTabId, opts) {
1757
1803
  case 'create-pipeline': {
1758
1804
  try {
1759
1805
  await _ccFetch('/api/pipelines', { id: action.id, title: action.title, stages: action.stages || [], trigger: action.trigger || null, stopWhen: action.stopWhen || null, monitoredResources: action.monitoredResources || null });
1806
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; pipeline id wrapped in escHtml() (fields: action.id)
1760
1807
  status.innerHTML = '&#10003; Pipeline created: <strong>' + escHtml(action.id) + '</strong>';
1761
1808
  status.style.color = 'var(--green)';
1762
1809
  } catch (e) {
1763
1810
  if (e.status === 409) {
1811
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; pipeline id wrapped in escHtml() (fields: action.id)
1764
1812
  status.innerHTML = '&#10003; Pipeline already exists: <strong>' + escHtml(action.id) + '</strong>';
1765
1813
  status.style.color = 'var(--orange)';
1766
1814
  } else {
@@ -1771,18 +1819,21 @@ async function ccExecuteAction(action, targetTabId, opts) {
1771
1819
  }
1772
1820
  case 'delete-pipeline': {
1773
1821
  await _ccFetch('/api/pipelines/delete', { id: action.id });
1822
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; pipeline id wrapped in escHtml() (fields: action.id)
1774
1823
  status.innerHTML = '&#10003; Pipeline deleted: <strong>' + escHtml(action.id) + '</strong>';
1775
1824
  status.style.color = 'var(--green)';
1776
1825
  break;
1777
1826
  }
1778
1827
  case 'abort-pipeline': {
1779
1828
  await _ccFetch('/api/pipelines/abort', { id: action.id });
1829
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; pipeline id wrapped in escHtml() (fields: action.id)
1780
1830
  status.innerHTML = '&#10003; Pipeline aborted: <strong>' + escHtml(action.id) + '</strong>';
1781
1831
  status.style.color = 'var(--green)';
1782
1832
  break;
1783
1833
  }
1784
1834
  case 'retrigger-pipeline': {
1785
1835
  await _ccFetch('/api/pipelines/retrigger', { id: action.id });
1836
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; pipeline id wrapped in escHtml() (fields: action.id)
1786
1837
  status.innerHTML = '&#10003; Pipeline retriggered: <strong>' + escHtml(action.id) + '</strong>';
1787
1838
  status.style.color = 'var(--green)';
1788
1839
  wakeEngine();
@@ -1790,6 +1841,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1790
1841
  }
1791
1842
  case 'advance-meeting': {
1792
1843
  await _ccFetch('/api/meetings/advance', { id: action.id });
1844
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; meeting id wrapped in escHtml() (fields: action.id)
1793
1845
  status.innerHTML = '&#10003; Meeting advanced: <strong>' + escHtml(action.id) + '</strong>';
1794
1846
  status.style.color = 'var(--green)';
1795
1847
  wakeEngine();
@@ -1797,18 +1849,21 @@ async function ccExecuteAction(action, targetTabId, opts) {
1797
1849
  }
1798
1850
  case 'end-meeting': {
1799
1851
  await _ccFetch('/api/meetings/end', { id: action.id });
1852
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; meeting id wrapped in escHtml() (fields: action.id)
1800
1853
  status.innerHTML = '&#10003; Meeting ended: <strong>' + escHtml(action.id) + '</strong>';
1801
1854
  status.style.color = 'var(--green)';
1802
1855
  break;
1803
1856
  }
1804
1857
  case 'delete-meeting': {
1805
1858
  await _ccFetch('/api/meetings/delete', { id: action.id });
1859
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; meeting id wrapped in escHtml() (fields: action.id)
1806
1860
  status.innerHTML = '&#10003; Meeting deleted: <strong>' + escHtml(action.id) + '</strong>';
1807
1861
  status.style.color = 'var(--green)';
1808
1862
  break;
1809
1863
  }
1810
1864
  case 'trigger-verify': {
1811
1865
  await _ccFetch('/api/plans/trigger-verify', { file: action.file });
1866
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; plan file wrapped in escHtml() (fields: action.file)
1812
1867
  status.innerHTML = '&#10003; Verification triggered for: <strong>' + escHtml(action.file) + '</strong>';
1813
1868
  status.style.color = 'var(--green)';
1814
1869
  wakeEngine();
@@ -1816,6 +1871,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1816
1871
  }
1817
1872
  case 'regenerate-plan': {
1818
1873
  await _ccFetch('/api/plans/approve', { file: action.file, forceRegen: true });
1874
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; plan file wrapped in escHtml() (fields: action.file)
1819
1875
  status.innerHTML = '&#10003; PRD regeneration queued: <strong>' + escHtml(action.file) + '</strong>';
1820
1876
  status.style.color = 'var(--green)';
1821
1877
  wakeEngine();
@@ -1823,12 +1879,14 @@ async function ccExecuteAction(action, targetTabId, opts) {
1823
1879
  }
1824
1880
  case 'unarchive-plan': {
1825
1881
  await _ccFetch('/api/plans/unarchive', { file: action.file });
1882
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; plan file wrapped in escHtml() (fields: action.file)
1826
1883
  status.innerHTML = '&#10003; Plan unarchived: <strong>' + escHtml(action.file) + '</strong>';
1827
1884
  status.style.color = 'var(--green)';
1828
1885
  break;
1829
1886
  }
1830
1887
  case 'continue-pipeline': {
1831
1888
  await _ccFetch('/api/pipelines/continue', { id: action.id });
1889
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; pipeline id wrapped in escHtml() (fields: action.id)
1832
1890
  status.innerHTML = '&#10003; Pipeline continued: <strong>' + escHtml(action.id) + '</strong>';
1833
1891
  status.style.color = 'var(--green)';
1834
1892
  wakeEngine();
@@ -1836,18 +1894,21 @@ async function ccExecuteAction(action, targetTabId, opts) {
1836
1894
  }
1837
1895
  case 'work-item-feedback': {
1838
1896
  await _ccFetch('/api/work-items/feedback', { id: action.id, rating: action.rating || 'up', comment: action.comment || '' });
1897
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; feedback id and rating wrapped in escHtml() (fields: action.id, action.rating)
1839
1898
  status.innerHTML = '&#10003; Feedback submitted for: <strong>' + escHtml(action.id) + '</strong> (' + escHtml(action.rating || 'up') + ')';
1840
1899
  status.style.color = 'var(--green)';
1841
1900
  break;
1842
1901
  }
1843
1902
  case 'archive-work-item': {
1844
1903
  await _ccFetch('/api/work-items/archive', { id: action.id });
1904
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; work item id wrapped in escHtml() (fields: action.id)
1845
1905
  status.innerHTML = '&#10003; Work item archived: <strong>' + escHtml(action.id) + '</strong>';
1846
1906
  status.style.color = 'var(--green)';
1847
1907
  break;
1848
1908
  }
1849
1909
  case 'reopen-work-item': {
1850
1910
  await _ccFetch('/api/work-items/reopen', { id: action.id, project: action.project || action.source || '', description: action.description });
1911
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; work item id wrapped in escHtml() (fields: action.id)
1851
1912
  status.innerHTML = '&#10003; Work item reopened: <strong>' + escHtml(action.id) + '</strong>';
1852
1913
  status.style.color = 'var(--green)';
1853
1914
  wakeEngine();
@@ -1855,6 +1916,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1855
1916
  }
1856
1917
  case 'reopen-prd-item': {
1857
1918
  await _ccFetch('/api/prd-items/update', { source: action.file, itemId: action.id, status: 'updated' });
1919
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; PRD item id wrapped in escHtml() (fields: action.id)
1858
1920
  status.innerHTML = '&#10003; PRD item reopened: <strong>' + escHtml(action.id) + '</strong>';
1859
1921
  status.style.color = 'var(--green)';
1860
1922
  wakeEngine();
@@ -1862,12 +1924,14 @@ async function ccExecuteAction(action, targetTabId, opts) {
1862
1924
  }
1863
1925
  case 'promote-to-kb': {
1864
1926
  await _ccFetch('/api/inbox/promote-kb', { name: action.file, category: action.category || 'project-notes' });
1927
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; inbox file name wrapped in escHtml() (fields: action.file)
1865
1928
  status.innerHTML = '&#10003; Promoted to KB: <strong>' + escHtml(action.file) + '</strong>';
1866
1929
  status.style.color = 'var(--green)';
1867
1930
  break;
1868
1931
  }
1869
1932
  case 'revise-plan': {
1870
1933
  await _ccFetch('/api/plans/revise', { file: action.file, feedback: action.feedback || action.description, requestedBy: 'command-center' });
1934
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; plan file wrapped in escHtml() (fields: action.file)
1871
1935
  status.innerHTML = '&#10003; Plan revision dispatched: <strong>' + escHtml(action.file) + '</strong>';
1872
1936
  status.style.color = 'var(--green)';
1873
1937
  wakeEngine();
@@ -1881,6 +1945,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1881
1945
  }
1882
1946
  case 'toggle-kb-pin': {
1883
1947
  await _ccFetch('/api/kb-pins/toggle', { key: action.key });
1948
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; KB key wrapped in escHtml() (fields: action.key)
1884
1949
  status.innerHTML = '&#10003; KB pin toggled: <strong>' + escHtml(action.key) + '</strong>';
1885
1950
  status.style.color = 'var(--green)';
1886
1951
  break;
@@ -1888,6 +1953,7 @@ async function ccExecuteAction(action, targetTabId, opts) {
1888
1953
  case 'add-project': {
1889
1954
  var projectPath = action.path || action.localPath;
1890
1955
  await _ccFetch('/api/projects/add', { path: projectPath, localPath: projectPath, name: action.name || '', repoHost: action.repoHost || 'github', allowNonRepo: action.allowNonRepo, confirmToken: action.confirmToken });
1956
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; project name/path wrapped in escHtml() (fields: action.name, projectPath)
1891
1957
  status.innerHTML = '&#10003; Project added: <strong>' + escHtml(action.name || projectPath) + '</strong>';
1892
1958
  status.style.color = 'var(--green)';
1893
1959
  break;
@@ -1900,12 +1966,14 @@ async function ccExecuteAction(action, targetTabId, opts) {
1900
1966
  }
1901
1967
  case 'delete-pr': {
1902
1968
  await _ccFetch('/api/pull-requests/delete', { id: action.id, project: action.project || '' });
1969
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; PR id wrapped in escHtml() (fields: action.id)
1903
1970
  status.innerHTML = '&#10003; PR unlinked: <strong>' + escHtml(action.id) + '</strong>';
1904
1971
  status.style.color = 'var(--green)';
1905
1972
  break;
1906
1973
  }
1907
1974
  case 'unarchive-meeting': {
1908
1975
  await _ccFetch('/api/meetings/unarchive', { id: action.id });
1976
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; meeting id wrapped in escHtml() (fields: action.id)
1909
1977
  status.innerHTML = '&#10003; Meeting unarchived: <strong>' + escHtml(action.id) + '</strong>';
1910
1978
  status.style.color = 'var(--green)';
1911
1979
  break;
@@ -1929,9 +1997,11 @@ async function ccExecuteAction(action, targetTabId, opts) {
1929
1997
  summary = 'response — ' + preview + (preview.length >= 240 ? '...' : '');
1930
1998
  }
1931
1999
  if (summary) {
2000
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; generic action type and response summary wrapped in escHtml() (fields: action.type, summary)
1932
2001
  status.innerHTML = '&#10003; ' + escHtml(action.type) + ': ' + escHtml(summary);
1933
2002
  status.style.color = 'var(--green)';
1934
2003
  } else {
2004
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; generic action type and endpoint wrapped in escHtml() (fields: action.type, action.endpoint)
1935
2005
  status.innerHTML = '? <strong>' + escHtml(action.type) + '</strong> hit ' + escHtml(action.endpoint) + ' but returned no data — answer inline next time, do not invent actions for read-only queries';
1936
2006
  status.style.color = 'var(--orange)';
1937
2007
  }
@@ -1939,12 +2009,14 @@ async function ccExecuteAction(action, targetTabId, opts) {
1939
2009
  status.innerHTML = '&#10007; Blocked: endpoint must be a local /api/ path';
1940
2010
  status.style.color = 'var(--red)';
1941
2011
  } else {
2012
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; unknown action type wrapped in escHtml() (fields: action.type)
1942
2013
  status.innerHTML = '? Unknown action: <strong>' + escHtml(action.type) + '</strong>';
1943
2014
  status.style.color = 'var(--muted)';
1944
2015
  }
1945
2016
  }
1946
2017
  }
1947
2018
  } catch (e) {
2019
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; action error message wrapped in escHtml() (fields: e.message)
1948
2020
  status.innerHTML = '&#10007; Action failed: ' + escHtml(e.message);
1949
2021
  status.style.color = 'var(--red)';
1950
2022
  }
@@ -27,6 +27,7 @@ function cmdShowHistory() {
27
27
  } else {
28
28
  const intentColors = { 'work-item': 'var(--blue)', 'note': 'var(--green)', 'plan': 'var(--purple,#a855f7)' };
29
29
  const intentLabels = { 'work-item': 'Work Item', 'note': 'Note', 'plan': 'Plan' };
30
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: command text, intent label)
30
31
  body.innerHTML = '<ul class="cmd-history-list">' + history.map((item, i) => {
31
32
  const date = new Date(item.timestamp);
32
33
  const ago = timeSinceStr(date);
@@ -36,7 +37,7 @@ function cmdShowHistory() {
36
37
  '<div class="cmd-history-item-body">' +
37
38
  '<div class="cmd-history-item-text">' + escHtml(item.text) + '</div>' +
38
39
  '<div class="cmd-history-item-meta">' +
39
- '<span class="chip" style="color:' + intentColor + '">' + intentLabel + '</span>' +
40
+ '<span class="chip" style="color:' + intentColor + '">' + escHtml(intentLabel) + '</span>' +
40
41
  '<span>' + ago + '</span>' +
41
42
  '<span>' + formatLocalDateTime(date) + '</span>' +
42
43
  '</div>' +
@@ -18,6 +18,7 @@ function cmdUpdateHighlight() {
18
18
  html = html.replace(/(![a-z]+\b)/gi, '<span class="hl-priority">$1</span>');
19
19
  html = html.replace(/(#\S+)/g, '<span class="hl-project">$1</span>');
20
20
  html = html.replace(/(--(?:stack|parallel)\b)/gi, '<span class="hl-flag">$1</span>');
21
+ // eslint-disable-next-line no-unsanitized/property -- reason: cmdUpdateHighlight() escapes all user-controlled text before assembling fixed highlight spans
21
22
  hl.innerHTML = html + '\n'; // trailing newline prevents layout shift
22
23
  }
23
24
 
@@ -179,7 +180,7 @@ function cmdRenderMeta() {
179
180
  for (const agentId of parsed.agents) {
180
181
  const agent = cmdAgents.find(a => a.id === agentId);
181
182
  if (agent) {
182
- chips.push('<span class="cmd-chip agent-chip">' + agent.emoji + ' @' + agent.name + '</span>');
183
+ chips.push('<span class="cmd-chip agent-chip">' + escHtml(agent.emoji || '') + ' @' + escHtml(agent.name) + '</span>');
183
184
  }
184
185
  }
185
186
 
@@ -190,6 +191,7 @@ function cmdRenderMeta() {
190
191
  chips.push('<span class="cmd-chip project-chip">#' + escHtml(parsed.project) + '</span>');
191
192
  }
192
193
 
194
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: project names, agent emoji, agent name)
193
195
  el.innerHTML = chips.join('');
194
196
  }
195
197
 
@@ -214,9 +216,10 @@ function cmdShowMentions(query) {
214
216
  if (items.length === 0) { popup.classList.remove('visible'); return; }
215
217
 
216
218
  cmdMentionIdx = 0;
219
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: agent id, emoji, name, role)
217
220
  popup.innerHTML = items.map((a, i) =>
218
- '<div class="cmd-mention-item' + (i === 0 ? ' active' : '') + '" data-id="' + a.id + '" onclick="cmdInsertPopupItem(\'' + escHtml(a.id) + '\')">' +
219
- '<span class="mention-emoji">' + a.emoji + '</span>' +
221
+ '<div class="cmd-mention-item' + (i === 0 ? ' active' : '') + '" data-id="' + escHtml(a.id) + '" onclick="cmdInsertPopupItem(\'' + escHtml(a.id) + '\')">' +
222
+ '<span class="mention-emoji">' + escHtml(a.emoji || '') + '</span>' +
220
223
  '<span class="mention-name">@' + escHtml(a.name) + '</span>' +
221
224
  '<span class="mention-role">' + escHtml(a.role) + '</span>' +
222
225
  '</div>'
@@ -234,6 +237,7 @@ function cmdShowProjects(query) {
234
237
  if (items.length === 0) { popup.classList.remove('visible'); return; }
235
238
 
236
239
  cmdMentionIdx = 0;
240
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() (fields: project name, description)
237
241
  popup.innerHTML = items.map((p, i) =>
238
242
  '<div class="cmd-mention-item' + (i === 0 ? ' active' : '') + '" data-id="' + escHtml(p.name) + '" onclick="cmdInsertPopupItem(\'' + escHtml(p.name) + '\')">' +
239
243
  '<span class="mention-emoji">\u{1F4C1}</span>' +
@@ -25,6 +25,7 @@ function showToast(id, msg, ok, durationMs) {
25
25
  el.classList.remove('success', 'error');
26
26
  el.classList.add(ok ? 'success' : 'error');
27
27
  if (msg.includes('<a ') || msg.includes('<strong>')) {
28
+ // eslint-disable-next-line no-unsanitized/property -- reason: structural HTML is a string literal; all user data wrapped in escHtml() before showToast() is invoked (fields: rich toast labels and URLs)
28
29
  el.innerHTML = msg;
29
30
  } else {
30
31
  el.textContent = msg;