@axiomatic-labs/claudeflow 2.13.66 → 2.13.68

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 (2) hide show
  1. package/lib/panel.js +109 -17
  2. package/package.json +1 -1
package/lib/panel.js CHANGED
@@ -331,11 +331,18 @@ function getObserverInfo(cwd) {
331
331
  const unresolvedIncidents = summary && Array.isArray(summary.unresolved_incidents)
332
332
  ? summary.unresolved_incidents
333
333
  : [];
334
- const allIncidents = state && state.incidents && typeof state.incidents === 'object'
334
+ const allInState = state && state.incidents && typeof state.incidents === 'object'
335
335
  ? Object.values(state.incidents)
336
336
  .sort((a, b) => String(b.last_seen_at || '').localeCompare(String(a.last_seen_at || '')))
337
337
  .slice(0, 50)
338
338
  : [];
339
+ // Default surface to UI: ACTIVE only. "Clear observer state" resolves all
340
+ // active incidents, so after clear the active list is empty (which matches
341
+ // the user's expectation that "Clear" empties the feed). Resolved incidents
342
+ // are kept in state.incidents for audit and can be surfaced via the
343
+ // "Show resolved" filter pill.
344
+ const activeIncidents = allInState.filter((inc) => !inc.resolved_at && inc.active !== false);
345
+ const resolvedIncidents = allInState.filter((inc) => inc.resolved_at || inc.active === false);
339
346
 
340
347
  return {
341
348
  daemon,
@@ -347,7 +354,10 @@ function getObserverInfo(cwd) {
347
354
  servers,
348
355
  browser,
349
356
  unresolved_incidents: unresolvedIncidents,
350
- recent_incidents: allIncidents,
357
+ active_incidents: activeIncidents,
358
+ resolved_incidents: resolvedIncidents,
359
+ // Kept for backwards compat with older render code — same as active_incidents.
360
+ recent_incidents: activeIncidents,
351
361
  totals: summary ? {
352
362
  unresolved: summary.unresolved_incidents_total || 0,
353
363
  browser: summary.browser_incidents_total || 0,
@@ -848,6 +858,7 @@ const SECTIONS = [
848
858
  let state = null;
849
859
  let active = 'overview';
850
860
  let observerFilter = 'all';
861
+ let observerShowResolved = false;
851
862
  let timer = null;
852
863
 
853
864
  // Preserve which <details> were open across re-renders (auto-refresh would
@@ -1020,6 +1031,13 @@ function row(k, v, badge) {
1020
1031
  return \`<div class="card-row"><span class="k">\${escapeHtml(k)}</span><span class="v">\${b}\${escapeHtml(v)}</span></div>\`;
1021
1032
  }
1022
1033
 
1034
+ // Same as row() but the value is rendered as raw HTML (caller is responsible
1035
+ // for escaping). Use for values that already contain HTML markup (e.g. <code>).
1036
+ function rowHtml(k, vHtml, badge) {
1037
+ const b = badge ? \`<span class="badge \${badge.kind}">\${escapeHtml(badge.text)}</span>\` : '';
1038
+ return \`<div class="card-row"><span class="k">\${escapeHtml(k)}</span><span class="v">\${b}\${vHtml}</span></div>\`;
1039
+ }
1040
+
1023
1041
  function escapeHtml(s) {
1024
1042
  return String(s == null ? '' : s)
1025
1043
  .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
@@ -1450,17 +1468,20 @@ function renderObserver() {
1450
1468
  // Browser section
1451
1469
  const browserBlock = o.browser && o.browser.last_snapshot_at
1452
1470
  ? \`<div class="card" style="padding:10px 14px;font-size:13px;">
1453
- \${row('Latest URL', o.browser.latest_url ? '<code>' + escapeHtml(o.browser.latest_url) + '</code>' : '<span class="muted">none</span>')}
1454
- \${row('Latest route', o.browser.latest_route ? '<code>' + escapeHtml(o.browser.latest_route) + '</code>' : '<span class="muted">none</span>')}
1455
- \${row('Snapshots received', String(o.browser.snapshots_received))}
1456
- \${row('Last snapshot', formatRelativeTime(o.browser.last_snapshot_at))}
1471
+ \${rowHtml('Latest URL', o.browser.latest_url ? '<code>' + escapeHtml(o.browser.latest_url) + '</code>' : '<span class="muted">none</span>')}
1472
+ \${rowHtml('Latest route', o.browser.latest_route ? '<code>' + escapeHtml(o.browser.latest_route) + '</code>' : '<span class="muted">none</span>')}
1473
+ \${rowHtml('Snapshots received', String(o.browser.snapshots_received))}
1474
+ \${rowHtml('Last snapshot', escapeHtml(formatRelativeTime(o.browser.last_snapshot_at)))}
1457
1475
  </div>\`
1458
1476
  : '<p class="muted">No browser snapshots received yet. (The observer collects when the agent navigates with mcp__claude-in-chrome__*.)</p>';
1459
1477
 
1460
- // Build the filter index: for each surface (server label), find which
1461
- // incidents belong to it. browser incidents are matched by origin URL
1462
- // against each server's :PORT. Browser-other = browser incidents whose
1463
- // origin doesn't match any configured server (e.g. external URL).
1478
+ // Active vs resolved toggle (client-side state observerShowResolved).
1479
+ // After "Clear observer state", all active incidents are marked resolved;
1480
+ // by default the UI shows only active. Toggle the pill to see history.
1481
+ const showResolved = (typeof observerShowResolved !== 'undefined') && observerShowResolved;
1482
+ const sourceList = showResolved ? o.resolved_incidents : o.active_incidents;
1483
+
1484
+ // Build the filter index ON THE CURRENT VIEW (active or resolved).
1464
1485
  const serverByOrigin = {};
1465
1486
  o.servers.forEach((srv) => {
1466
1487
  if (srv.port) serverByOrigin[\`localhost:\${srv.port}\`] = srv.label;
@@ -1479,7 +1500,7 @@ function renderObserver() {
1479
1500
  return inc.source || 'other';
1480
1501
  }
1481
1502
  const buckets = {};
1482
- o.recent_incidents.forEach((inc) => {
1503
+ sourceList.forEach((inc) => {
1483
1504
  const b = incidentBucket(inc);
1484
1505
  if (!buckets[b]) buckets[b] = [];
1485
1506
  buckets[b].push(inc);
@@ -1487,7 +1508,7 @@ function renderObserver() {
1487
1508
  const surfacesInUse = Object.keys(buckets).sort();
1488
1509
  // Apply observerFilter client-side state. Default 'all' (no filter).
1489
1510
  const filteredIncidents = (typeof observerFilter === 'undefined' || observerFilter === 'all')
1490
- ? o.recent_incidents
1511
+ ? sourceList
1491
1512
  : (buckets[observerFilter] || []);
1492
1513
 
1493
1514
  const filterPill = (label, value, count) => {
@@ -1495,11 +1516,42 @@ function renderObserver() {
1495
1516
  const cls = active ? 'observer-filter active' : 'observer-filter';
1496
1517
  return \`<button class="\${cls}" data-observer-filter="\${escapeHtml(value)}">\${escapeHtml(label)}<span class="muted" style="margin-left:6px;font-size:11px;">\${count}</span></button>\`;
1497
1518
  };
1498
- const filterPills = '<div class="observer-filters" style="display:flex;gap:6px;flex-wrap:wrap;margin:8px 0;">' +
1499
- filterPill('All', 'all', o.recent_incidents.length) +
1519
+ // Show resolved toggle pill separate from surface filters.
1520
+ const showResolvedClass = showResolved ? 'observer-filter active' : 'observer-filter';
1521
+ const showResolvedPill = \`<button class="\${showResolvedClass}" data-observer-show-resolved="toggle" style="margin-left:auto;">\${showResolved ? 'Showing resolved' : 'Show resolved'}<span class="muted" style="margin-left:6px;font-size:11px;">\${o.resolved_incidents.length}</span></button>\`;
1522
+ const filterPills = '<div class="observer-filters" style="display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin:8px 0;">' +
1523
+ filterPill('All', 'all', sourceList.length) +
1500
1524
  surfacesInUse.map((surface) => filterPill(surface, surface, buckets[surface].length)).join('') +
1525
+ showResolvedPill +
1501
1526
  '</div>';
1502
1527
 
1528
+ // Match file:line patterns in stack traces (most common formats):
1529
+ // http://localhost:3001/src/foo.tsx:51:22
1530
+ // /Users/abs/path.ts:42
1531
+ // at SomeFn (path:line:col)
1532
+ function extractFileLineHints(text) {
1533
+ if (typeof text !== 'string') return [];
1534
+ const hints = new Set();
1535
+ // localhost source URLs (Vite/CRA dev server)
1536
+ const localRe = /https?:\\/\\/localhost(?::\\d+)?\\/([^\\s:]+?\\.(?:tsx?|jsx?|vue|svelte)):(\\d+)(?::(\\d+))?/g;
1537
+ let m;
1538
+ while ((m = localRe.exec(text)) !== null) {
1539
+ hints.add(\`\${m[1]}:\${m[2]}\${m[3] ? ':' + m[3] : ''}\`);
1540
+ if (hints.size >= 6) break;
1541
+ }
1542
+ // Bare absolute paths
1543
+ const absRe = /\\b(\\/[A-Za-z0-9_./-]+?\\.(?:tsx?|jsx?|vue|svelte)):(\\d+)(?::(\\d+))?/g;
1544
+ while ((m = absRe.exec(text)) !== null) {
1545
+ hints.add(\`\${m[1]}:\${m[2]}\${m[3] ? ':' + m[3] : ''}\`);
1546
+ if (hints.size >= 6) break;
1547
+ }
1548
+ return [...hints];
1549
+ }
1550
+
1551
+ // Server-side detail (recent_log + exit info) lookup per surface label.
1552
+ const serverByLabel = {};
1553
+ o.servers.forEach((srv) => { serverByLabel[srv.label] = srv; });
1554
+
1503
1555
  // Incidents feed
1504
1556
  const incidentsBlock = filteredIncidents.length === 0
1505
1557
  ? '<p class="muted">No incidents in this filter.</p>'
@@ -1512,6 +1564,36 @@ function renderObserver() {
1512
1564
  const surfaceLabel = inc.source === 'server' && inc.metadata && inc.metadata.label
1513
1565
  ? inc.metadata.label
1514
1566
  : inc.surface || inc.source;
1567
+ const fullMsg = inc.message || '';
1568
+ const fileLineHints = extractFileLineHints(fullMsg);
1569
+ // Show first 240 chars by default, collapsible to full message.
1570
+ const PREVIEW_CHARS = 240;
1571
+ const isLong = fullMsg.length > PREVIEW_CHARS;
1572
+ const previewMsg = isLong ? fullMsg.slice(0, PREVIEW_CHARS) + '…' : fullMsg;
1573
+ const fullBlock = isLong
1574
+ ? '<details style="margin-top:4px;"><summary style="cursor:pointer;color:var(--muted);font-size:11px;">Show full message + stack (' + fullMsg.length + ' chars)</summary><pre style="font-size:11px;white-space:pre-wrap;background:var(--panel-2);padding:8px;border-radius:4px;margin:4px 0 0 0;overflow:auto;max-height:400px;">' + escapeHtml(fullMsg) + '</pre></details>'
1575
+ : '';
1576
+ const fileLinesHtml = fileLineHints.length > 0
1577
+ ? '<div class="muted" style="font-size:11px;margin-top:4px;">📄 ' + fileLineHints.map((f) => '<code>' + escapeHtml(f) + '</code>').join(' • ') + '</div>'
1578
+ : '';
1579
+ const urlLine = inc.metadata && inc.metadata.url
1580
+ ? '<div class="muted" style="font-size:11px;margin-top:4px;">🌐 <code>' + escapeHtml(inc.metadata.url) + '</code>' + (inc.metadata.route && inc.metadata.route !== inc.metadata.url ? ' (' + escapeHtml(inc.metadata.route) + ')' : '') + '</div>'
1581
+ : '';
1582
+ // For server incidents, attach recent_log + exit info from the matching server record.
1583
+ let serverDetail = '';
1584
+ if (inc.source === 'server') {
1585
+ const srv = serverByLabel[surfaceLabel];
1586
+ if (srv) {
1587
+ const exitInfo = srv.last_exit_code !== null && srv.last_exit_code !== undefined
1588
+ ? \`<div class="muted" style="font-size:11px;margin-top:4px;">🛑 last exit: code \${srv.last_exit_code}\${srv.last_exit_signal ? ' / signal ' + srv.last_exit_signal : ''} at \${escapeHtml(formatRelativeTime(srv.last_exit_at))}</div>\`
1589
+ : '';
1590
+ const logLines = Array.isArray(srv.recent_log) ? srv.recent_log : [];
1591
+ const logBlock = logLines.length > 0
1592
+ ? '<details style="margin-top:4px;"><summary style="cursor:pointer;color:var(--muted);font-size:11px;">📋 recent_log (' + logLines.length + ' lines from daemon-supervised stdout/stderr)</summary><pre style="font-size:11px;white-space:pre-wrap;background:var(--panel-2);padding:8px;border-radius:4px;margin:4px 0 0 0;overflow:auto;max-height:300px;">' + logLines.slice(-50).map((l) => escapeHtml(typeof l === 'string' ? l : (l.line || JSON.stringify(l)))).join('\\n') + '</pre></details>'
1593
+ : '<div class="muted" style="font-size:11px;margin-top:4px;">📋 No recent_log captured (server is observed_external — daemon only port-probes it, doesn\\'t pipe stdout/stderr). Run the server under the daemon supervisor to capture log lines, or check the dev server\\'s own terminal output.</div>';
1594
+ serverDetail = exitInfo + logBlock;
1595
+ }
1596
+ }
1515
1597
  return \`<div class="incident-row" style="padding:10px 14px;border-top:1px solid var(--border);">
1516
1598
  <div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:4px;">
1517
1599
  <span class="badge \${sevClass}">\${escapeHtml(inc.severity || 'info')}</span>
@@ -1522,8 +1604,11 @@ function renderObserver() {
1522
1604
  \${resolvedBadge}
1523
1605
  <span class="muted" style="margin-left:auto;font-size:11px;">\${escapeHtml(formatRelativeTime(inc.last_seen_at))}</span>
1524
1606
  </div>
1525
- <div style="font-size:12px;white-space:pre-wrap;">\${escapeHtml((inc.message || '').slice(0, 600))}\${(inc.message || '').length > 600 ? '…' : ''}</div>
1526
- \${inc.metadata && inc.metadata.url ? '<div class="muted" style="font-size:11px;margin-top:4px;">at <code>' + escapeHtml(inc.metadata.url) + '</code></div>' : ''}
1607
+ <div style="font-size:12px;white-space:pre-wrap;">\${escapeHtml(previewMsg)}</div>
1608
+ \${fullBlock}
1609
+ \${fileLinesHtml}
1610
+ \${urlLine}
1611
+ \${serverDetail}
1527
1612
  </div>\`;
1528
1613
  }).join('') +
1529
1614
  '</div>';
@@ -1542,7 +1627,7 @@ function renderObserver() {
1542
1627
  <div class="card" style="padding:0;">\${serversTable}</div>
1543
1628
  <h3 style="margin-top:18px;font-size:14px;">Browser channel</h3>
1544
1629
  \${browserBlock}
1545
- <h3 style="margin-top:18px;font-size:14px;">Incidents (most recent \${o.recent_incidents.length})</h3>
1630
+ <h3 style="margin-top:18px;font-size:14px;">\${showResolved ? 'Resolved incidents (' + o.resolved_incidents.length + ')' : 'Active incidents (' + o.active_incidents.length + ')'}</h3>
1546
1631
  \${filterPills}
1547
1632
  <div class="card" style="padding:0;">\${incidentsBlock}</div>\`;
1548
1633
  }
@@ -1699,6 +1784,13 @@ document.addEventListener('click', (e) => {
1699
1784
  renderContent();
1700
1785
  return;
1701
1786
  }
1787
+ if (t.dataset && t.dataset.observerShowResolved) {
1788
+ observerShowResolved = !observerShowResolved;
1789
+ // Reset surface filter when toggling, since counts change.
1790
+ observerFilter = 'all';
1791
+ renderContent();
1792
+ return;
1793
+ }
1702
1794
  });
1703
1795
 
1704
1796
  async function clearObserverAction(btn) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axiomatic-labs/claudeflow",
3
- "version": "2.13.66",
3
+ "version": "2.13.68",
4
4
  "description": "Claudeflow — AI-powered development toolkit for Claude Code. Skills, agents, hooks, and quality gates that ship production apps.",
5
5
  "bin": {
6
6
  "claudeflow": "./bin/cli.js"