agentflow-dashboard 0.4.0 → 0.4.1

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.
@@ -1667,6 +1667,98 @@ var DashboardServer = class {
1667
1667
  res.status(500).json({ error: "Failed to load statistics" });
1668
1668
  }
1669
1669
  });
1670
+ this.app.get("/api/agents/:agentId/process-graph", (req, res) => {
1671
+ try {
1672
+ const agentId = req.params.agentId;
1673
+ const traces = this.watcher.getTracesByAgent(agentId).map(serializeTrace);
1674
+ if (traces.length === 0) {
1675
+ return res.status(404).json({ error: "No traces for agent" });
1676
+ }
1677
+ const activityCounts = /* @__PURE__ */ new Map();
1678
+ const transitionCounts = /* @__PURE__ */ new Map();
1679
+ const activityDurations = /* @__PURE__ */ new Map();
1680
+ const activityStatuses = /* @__PURE__ */ new Map();
1681
+ let totalTraces = 0;
1682
+ for (const trace of traces) {
1683
+ totalTraces++;
1684
+ const activities = [];
1685
+ if (trace.sessionEvents && trace.sessionEvents.length > 0) {
1686
+ for (const evt of trace.sessionEvents) {
1687
+ const name = evt.toolName || evt.name || evt.type;
1688
+ if (!name) continue;
1689
+ activities.push({
1690
+ name,
1691
+ type: evt.type,
1692
+ status: evt.toolError ? "failed" : "completed",
1693
+ duration: evt.duration || 0
1694
+ });
1695
+ }
1696
+ } else {
1697
+ const nodes2 = trace.nodes || {};
1698
+ const sorted = Object.values(nodes2).sort((a, b) => (a.startTime || 0) - (b.startTime || 0));
1699
+ for (const node of sorted) {
1700
+ activities.push({
1701
+ name: node.name || node.type || node.id,
1702
+ type: node.type || "unknown",
1703
+ status: node.status || "completed",
1704
+ duration: (node.endTime || node.startTime || 0) - (node.startTime || 0)
1705
+ });
1706
+ }
1707
+ }
1708
+ const seq = ["[START]", ...activities.map((a) => a.name), "[END]"];
1709
+ for (let i = 0; i < seq.length; i++) {
1710
+ const act = seq[i];
1711
+ activityCounts.set(act, (activityCounts.get(act) || 0) + 1);
1712
+ if (i < seq.length - 1) {
1713
+ const key = act + " \u2192 " + seq[i + 1];
1714
+ transitionCounts.set(key, (transitionCounts.get(key) || 0) + 1);
1715
+ }
1716
+ }
1717
+ for (const act of activities) {
1718
+ if (act.duration > 0) {
1719
+ const durs = activityDurations.get(act.name) || [];
1720
+ durs.push(act.duration);
1721
+ activityDurations.set(act.name, durs);
1722
+ }
1723
+ const st = activityStatuses.get(act.name) || { ok: 0, fail: 0 };
1724
+ if (act.status === "failed") st.fail++;
1725
+ else st.ok++;
1726
+ activityStatuses.set(act.name, st);
1727
+ }
1728
+ }
1729
+ const nodes = Array.from(activityCounts.entries()).map(([name, count]) => {
1730
+ const durs = activityDurations.get(name) || [];
1731
+ const st = activityStatuses.get(name) || { ok: 0, fail: 0 };
1732
+ const avgDuration = durs.length > 0 ? durs.reduce((a, b) => a + b, 0) / durs.length : 0;
1733
+ return {
1734
+ id: name,
1735
+ label: name,
1736
+ count,
1737
+ frequency: count / totalTraces,
1738
+ avgDuration,
1739
+ failRate: st.ok + st.fail > 0 ? st.fail / (st.ok + st.fail) : 0,
1740
+ isVirtual: name === "[START]" || name === "[END]"
1741
+ };
1742
+ });
1743
+ const edges = Array.from(transitionCounts.entries()).map(([key, count]) => {
1744
+ const [source, target] = key.split(" \u2192 ");
1745
+ return { source, target, count, frequency: count / totalTraces };
1746
+ });
1747
+ const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
1748
+ const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
1749
+ res.json({
1750
+ agentId,
1751
+ totalTraces,
1752
+ nodes,
1753
+ edges,
1754
+ maxEdgeCount,
1755
+ maxNodeCount
1756
+ });
1757
+ } catch (error) {
1758
+ console.error("Process graph error:", error);
1759
+ res.status(500).json({ error: "Failed to build process graph" });
1760
+ }
1761
+ });
1670
1762
  this.app.get("/api/stats/:agentId", (req, res) => {
1671
1763
  try {
1672
1764
  const agentStats = this.stats.getAgentStats(req.params.agentId);
package/dist/cli.cjs CHANGED
@@ -1561,6 +1561,98 @@ var DashboardServer = class {
1561
1561
  res.status(500).json({ error: "Failed to load statistics" });
1562
1562
  }
1563
1563
  });
1564
+ this.app.get("/api/agents/:agentId/process-graph", (req, res) => {
1565
+ try {
1566
+ const agentId = req.params.agentId;
1567
+ const traces = this.watcher.getTracesByAgent(agentId).map(serializeTrace);
1568
+ if (traces.length === 0) {
1569
+ return res.status(404).json({ error: "No traces for agent" });
1570
+ }
1571
+ const activityCounts = /* @__PURE__ */ new Map();
1572
+ const transitionCounts = /* @__PURE__ */ new Map();
1573
+ const activityDurations = /* @__PURE__ */ new Map();
1574
+ const activityStatuses = /* @__PURE__ */ new Map();
1575
+ let totalTraces = 0;
1576
+ for (const trace of traces) {
1577
+ totalTraces++;
1578
+ const activities = [];
1579
+ if (trace.sessionEvents && trace.sessionEvents.length > 0) {
1580
+ for (const evt of trace.sessionEvents) {
1581
+ const name = evt.toolName || evt.name || evt.type;
1582
+ if (!name) continue;
1583
+ activities.push({
1584
+ name,
1585
+ type: evt.type,
1586
+ status: evt.toolError ? "failed" : "completed",
1587
+ duration: evt.duration || 0
1588
+ });
1589
+ }
1590
+ } else {
1591
+ const nodes2 = trace.nodes || {};
1592
+ const sorted = Object.values(nodes2).sort((a, b) => (a.startTime || 0) - (b.startTime || 0));
1593
+ for (const node of sorted) {
1594
+ activities.push({
1595
+ name: node.name || node.type || node.id,
1596
+ type: node.type || "unknown",
1597
+ status: node.status || "completed",
1598
+ duration: (node.endTime || node.startTime || 0) - (node.startTime || 0)
1599
+ });
1600
+ }
1601
+ }
1602
+ const seq = ["[START]", ...activities.map((a) => a.name), "[END]"];
1603
+ for (let i = 0; i < seq.length; i++) {
1604
+ const act = seq[i];
1605
+ activityCounts.set(act, (activityCounts.get(act) || 0) + 1);
1606
+ if (i < seq.length - 1) {
1607
+ const key = act + " \u2192 " + seq[i + 1];
1608
+ transitionCounts.set(key, (transitionCounts.get(key) || 0) + 1);
1609
+ }
1610
+ }
1611
+ for (const act of activities) {
1612
+ if (act.duration > 0) {
1613
+ const durs = activityDurations.get(act.name) || [];
1614
+ durs.push(act.duration);
1615
+ activityDurations.set(act.name, durs);
1616
+ }
1617
+ const st = activityStatuses.get(act.name) || { ok: 0, fail: 0 };
1618
+ if (act.status === "failed") st.fail++;
1619
+ else st.ok++;
1620
+ activityStatuses.set(act.name, st);
1621
+ }
1622
+ }
1623
+ const nodes = Array.from(activityCounts.entries()).map(([name, count]) => {
1624
+ const durs = activityDurations.get(name) || [];
1625
+ const st = activityStatuses.get(name) || { ok: 0, fail: 0 };
1626
+ const avgDuration = durs.length > 0 ? durs.reduce((a, b) => a + b, 0) / durs.length : 0;
1627
+ return {
1628
+ id: name,
1629
+ label: name,
1630
+ count,
1631
+ frequency: count / totalTraces,
1632
+ avgDuration,
1633
+ failRate: st.ok + st.fail > 0 ? st.fail / (st.ok + st.fail) : 0,
1634
+ isVirtual: name === "[START]" || name === "[END]"
1635
+ };
1636
+ });
1637
+ const edges = Array.from(transitionCounts.entries()).map(([key, count]) => {
1638
+ const [source, target] = key.split(" \u2192 ");
1639
+ return { source, target, count, frequency: count / totalTraces };
1640
+ });
1641
+ const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
1642
+ const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
1643
+ res.json({
1644
+ agentId,
1645
+ totalTraces,
1646
+ nodes,
1647
+ edges,
1648
+ maxEdgeCount,
1649
+ maxNodeCount
1650
+ });
1651
+ } catch (error) {
1652
+ console.error("Process graph error:", error);
1653
+ res.status(500).json({ error: "Failed to build process graph" });
1654
+ }
1655
+ });
1564
1656
  this.app.get("/api/stats/:agentId", (req, res) => {
1565
1657
  try {
1566
1658
  const agentStats = this.stats.getAgentStats(req.params.agentId);
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  startDashboard
3
- } from "./chunk-25MUPUYY.js";
3
+ } from "./chunk-YDFLDRWO.js";
4
4
  export {
5
5
  startDashboard
6
6
  };
package/dist/index.cjs CHANGED
@@ -1706,6 +1706,98 @@ var DashboardServer = class {
1706
1706
  res.status(500).json({ error: "Failed to load statistics" });
1707
1707
  }
1708
1708
  });
1709
+ this.app.get("/api/agents/:agentId/process-graph", (req, res) => {
1710
+ try {
1711
+ const agentId = req.params.agentId;
1712
+ const traces = this.watcher.getTracesByAgent(agentId).map(serializeTrace);
1713
+ if (traces.length === 0) {
1714
+ return res.status(404).json({ error: "No traces for agent" });
1715
+ }
1716
+ const activityCounts = /* @__PURE__ */ new Map();
1717
+ const transitionCounts = /* @__PURE__ */ new Map();
1718
+ const activityDurations = /* @__PURE__ */ new Map();
1719
+ const activityStatuses = /* @__PURE__ */ new Map();
1720
+ let totalTraces = 0;
1721
+ for (const trace of traces) {
1722
+ totalTraces++;
1723
+ const activities = [];
1724
+ if (trace.sessionEvents && trace.sessionEvents.length > 0) {
1725
+ for (const evt of trace.sessionEvents) {
1726
+ const name = evt.toolName || evt.name || evt.type;
1727
+ if (!name) continue;
1728
+ activities.push({
1729
+ name,
1730
+ type: evt.type,
1731
+ status: evt.toolError ? "failed" : "completed",
1732
+ duration: evt.duration || 0
1733
+ });
1734
+ }
1735
+ } else {
1736
+ const nodes2 = trace.nodes || {};
1737
+ const sorted = Object.values(nodes2).sort((a, b) => (a.startTime || 0) - (b.startTime || 0));
1738
+ for (const node of sorted) {
1739
+ activities.push({
1740
+ name: node.name || node.type || node.id,
1741
+ type: node.type || "unknown",
1742
+ status: node.status || "completed",
1743
+ duration: (node.endTime || node.startTime || 0) - (node.startTime || 0)
1744
+ });
1745
+ }
1746
+ }
1747
+ const seq = ["[START]", ...activities.map((a) => a.name), "[END]"];
1748
+ for (let i = 0; i < seq.length; i++) {
1749
+ const act = seq[i];
1750
+ activityCounts.set(act, (activityCounts.get(act) || 0) + 1);
1751
+ if (i < seq.length - 1) {
1752
+ const key = act + " \u2192 " + seq[i + 1];
1753
+ transitionCounts.set(key, (transitionCounts.get(key) || 0) + 1);
1754
+ }
1755
+ }
1756
+ for (const act of activities) {
1757
+ if (act.duration > 0) {
1758
+ const durs = activityDurations.get(act.name) || [];
1759
+ durs.push(act.duration);
1760
+ activityDurations.set(act.name, durs);
1761
+ }
1762
+ const st = activityStatuses.get(act.name) || { ok: 0, fail: 0 };
1763
+ if (act.status === "failed") st.fail++;
1764
+ else st.ok++;
1765
+ activityStatuses.set(act.name, st);
1766
+ }
1767
+ }
1768
+ const nodes = Array.from(activityCounts.entries()).map(([name, count]) => {
1769
+ const durs = activityDurations.get(name) || [];
1770
+ const st = activityStatuses.get(name) || { ok: 0, fail: 0 };
1771
+ const avgDuration = durs.length > 0 ? durs.reduce((a, b) => a + b, 0) / durs.length : 0;
1772
+ return {
1773
+ id: name,
1774
+ label: name,
1775
+ count,
1776
+ frequency: count / totalTraces,
1777
+ avgDuration,
1778
+ failRate: st.ok + st.fail > 0 ? st.fail / (st.ok + st.fail) : 0,
1779
+ isVirtual: name === "[START]" || name === "[END]"
1780
+ };
1781
+ });
1782
+ const edges = Array.from(transitionCounts.entries()).map(([key, count]) => {
1783
+ const [source, target] = key.split(" \u2192 ");
1784
+ return { source, target, count, frequency: count / totalTraces };
1785
+ });
1786
+ const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
1787
+ const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
1788
+ res.json({
1789
+ agentId,
1790
+ totalTraces,
1791
+ nodes,
1792
+ edges,
1793
+ maxEdgeCount,
1794
+ maxNodeCount
1795
+ });
1796
+ } catch (error) {
1797
+ console.error("Process graph error:", error);
1798
+ res.status(500).json({ error: "Failed to build process graph" });
1799
+ }
1800
+ });
1709
1801
  this.app.get("/api/stats/:agentId", (req, res) => {
1710
1802
  try {
1711
1803
  const agentStats = this.stats.getAgentStats(req.params.agentId);
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  AgentStats,
4
4
  DashboardServer,
5
5
  TraceWatcher
6
- } from "./chunk-25MUPUYY.js";
6
+ } from "./chunk-YDFLDRWO.js";
7
7
  export {
8
8
  AgentStats,
9
9
  DashboardServer,
@@ -307,6 +307,12 @@ class AgentFlowDashboard {
307
307
  this.selectedTrace = trace;
308
308
  this.selectedTraceData = trace;
309
309
 
310
+ // Reset process map cache when agent changes
311
+ if (this._processMapAgent !== trace.agentId) {
312
+ this._processMapAgent = null;
313
+ if (this._cyProcessMap) { this._cyProcessMap.destroy(); this._cyProcessMap = null; }
314
+ }
315
+
310
316
  // Update sidebar selection
311
317
  document.querySelectorAll('.session-item').forEach(function(el) { el.classList.remove('active'); });
312
318
  var activeEl = document.querySelector('.session-item[data-filename="' + CSS.escape(filename) + '"]');
@@ -850,6 +856,7 @@ class AgentFlowDashboard {
850
856
  case 'state': this.renderStateMachine(); break;
851
857
  case 'summary': this.renderSummary(); break;
852
858
  case 'transcript': this.renderTranscript(); break;
859
+ case 'processmap': this.renderProcessMap(); break;
853
860
  }
854
861
  this.updateToolbarInfo();
855
862
  }
@@ -1906,6 +1913,216 @@ class AgentFlowDashboard {
1906
1913
 
1907
1914
  return 'other';
1908
1915
  }
1916
+
1917
+ // ---------------------------------------------------------------------------
1918
+ // Tab 8: Process Map (Process Mining Graph)
1919
+ // ---------------------------------------------------------------------------
1920
+ renderProcessMap() {
1921
+ var trace = this.selectedTraceData || this.selectedTrace;
1922
+ if (!trace || !trace.agentId) {
1923
+ document.getElementById('processMapEmpty').style.display = '';
1924
+ return;
1925
+ }
1926
+
1927
+ var agentId = trace.agentId;
1928
+ var self = this;
1929
+
1930
+ // Avoid re-fetching for same agent
1931
+ if (this._processMapAgent === agentId && this._cyProcessMap) return;
1932
+ this._processMapAgent = agentId;
1933
+
1934
+ document.getElementById('processMapEmpty').innerHTML =
1935
+ '<div class="empty-state-icon" style="animation:spin 1s linear infinite">&#9881;</div>' +
1936
+ '<div class="empty-state-text">Building process map for ' + escapeHtml(agentId) + '...</div>';
1937
+ document.getElementById('processMapEmpty').style.display = '';
1938
+
1939
+ fetch('/api/agents/' + encodeURIComponent(agentId) + '/process-graph')
1940
+ .then(function(r) { return r.json(); })
1941
+ .then(function(data) {
1942
+ if (data.error || !data.nodes || data.nodes.length === 0) {
1943
+ document.getElementById('processMapEmpty').innerHTML =
1944
+ '<div class="empty-state-icon">&#9881;</div>' +
1945
+ '<div class="empty-state-text">No process data for ' + escapeHtml(agentId) + '</div>';
1946
+ return;
1947
+ }
1948
+ document.getElementById('processMapEmpty').style.display = 'none';
1949
+ self._buildProcessMapGraph(data);
1950
+ })
1951
+ .catch(function() {
1952
+ document.getElementById('processMapEmpty').innerHTML =
1953
+ '<div class="empty-state-icon">&#9881;</div>' +
1954
+ '<div class="empty-state-text">Failed to load process map.</div>';
1955
+ });
1956
+ }
1957
+
1958
+ _buildProcessMapGraph(data) {
1959
+ if (this._cyProcessMap) { this._cyProcessMap.destroy(); this._cyProcessMap = null; }
1960
+
1961
+ var elements = [];
1962
+ var maxNode = data.maxNodeCount || 1;
1963
+ var maxEdge = data.maxEdgeCount || 1;
1964
+
1965
+ // Add nodes
1966
+ for (var i = 0; i < data.nodes.length; i++) {
1967
+ var node = data.nodes[i];
1968
+ // Skip very rare activities (< 2% frequency) to reduce clutter, but keep virtual nodes
1969
+ if (!node.isVirtual && node.frequency < 0.02 && data.nodes.length > 15) continue;
1970
+
1971
+ var size = node.isVirtual ? 30 : Math.max(25, Math.min(70, 25 + 45 * (node.count / maxNode)));
1972
+ var label = node.label;
1973
+ if (!node.isVirtual && node.count > 1) label += ' (' + node.count + ')';
1974
+
1975
+ elements.push({
1976
+ group: 'nodes',
1977
+ data: {
1978
+ id: node.id,
1979
+ label: label,
1980
+ count: node.count,
1981
+ frequency: node.frequency,
1982
+ avgDuration: node.avgDuration,
1983
+ failRate: node.failRate,
1984
+ isVirtual: node.isVirtual,
1985
+ size: size,
1986
+ fullData: node
1987
+ }
1988
+ });
1989
+ }
1990
+
1991
+ // Collect valid node IDs
1992
+ var validIds = new Set(elements.map(function(e) { return e.data.id; }));
1993
+
1994
+ // Add edges (only between valid nodes)
1995
+ for (var j = 0; j < data.edges.length; j++) {
1996
+ var edge = data.edges[j];
1997
+ if (!validIds.has(edge.source) || !validIds.has(edge.target)) continue;
1998
+ // Skip very rare transitions
1999
+ if (edge.frequency < 0.02 && data.edges.length > 30) continue;
2000
+
2001
+ var width = Math.max(1, Math.min(8, 1 + 7 * (edge.count / maxEdge)));
2002
+ var opacity = Math.max(0.3, Math.min(1.0, 0.3 + 0.7 * (edge.count / maxEdge)));
2003
+
2004
+ elements.push({
2005
+ group: 'edges',
2006
+ data: {
2007
+ id: 'pe-' + edge.source + '-' + edge.target,
2008
+ source: edge.source,
2009
+ target: edge.target,
2010
+ count: edge.count,
2011
+ frequency: edge.frequency,
2012
+ width: width,
2013
+ opacity: opacity,
2014
+ label: edge.count > 1 ? String(edge.count) : ''
2015
+ }
2016
+ });
2017
+ }
2018
+
2019
+ var container = document.getElementById('cyProcessMap');
2020
+ var self = this;
2021
+
2022
+ this._cyProcessMap = cytoscape({
2023
+ container: container,
2024
+ elements: elements,
2025
+ style: [
2026
+ {
2027
+ selector: 'node',
2028
+ style: {
2029
+ 'label': 'data(label)',
2030
+ 'width': 'data(size)',
2031
+ 'height': 'data(size)',
2032
+ 'font-size': '9px',
2033
+ 'text-valign': 'bottom',
2034
+ 'text-halign': 'center',
2035
+ 'text-margin-y': 6,
2036
+ 'color': '#c9d1d9',
2037
+ 'text-outline-color': '#0d1117',
2038
+ 'text-outline-width': 2,
2039
+ 'text-wrap': 'ellipsis',
2040
+ 'text-max-width': '100px',
2041
+ 'border-width': 2,
2042
+ 'border-color': '#30363d',
2043
+ 'background-color': '#3b82f6',
2044
+ 'shape': 'round-rectangle'
2045
+ }
2046
+ },
2047
+ // Virtual START/END nodes
2048
+ { selector: 'node[?isVirtual]', style: {
2049
+ 'background-color': '#6b7280', 'shape': 'ellipse', 'border-color': '#4b5563',
2050
+ 'font-size': '8px', 'font-weight': 'bold', 'text-valign': 'center', 'text-margin-y': 0
2051
+ }},
2052
+ // Color by fail rate: green → yellow → red
2053
+ { selector: 'node[failRate <= 0]', style: { 'background-color': '#10b981', 'border-color': '#2ea043' }},
2054
+ { selector: 'node[failRate > 0][failRate <= 0.1]', style: { 'background-color': '#22c55e', 'border-color': '#3fb950' }},
2055
+ { selector: 'node[failRate > 0.1][failRate <= 0.3]', style: { 'background-color': '#eab308', 'border-color': '#d29922' }},
2056
+ { selector: 'node[failRate > 0.3]', style: { 'background-color': '#ef4444', 'border-color': '#f85149' }},
2057
+ // Selected
2058
+ { selector: ':selected', style: { 'border-width': 4, 'border-color': '#f59e0b', 'overlay-opacity': 0.08 }},
2059
+ // Edges
2060
+ {
2061
+ selector: 'edge',
2062
+ style: {
2063
+ 'width': 'data(width)',
2064
+ 'opacity': 'data(opacity)',
2065
+ 'line-color': '#6b7280',
2066
+ 'target-arrow-color': '#6b7280',
2067
+ 'target-arrow-shape': 'triangle',
2068
+ 'curve-style': 'bezier',
2069
+ 'arrow-scale': 0.7,
2070
+ 'label': 'data(label)',
2071
+ 'font-size': '8px',
2072
+ 'color': '#8b949e',
2073
+ 'text-outline-color': '#0d1117',
2074
+ 'text-outline-width': 1.5,
2075
+ 'text-rotation': 'autorotate'
2076
+ }
2077
+ }
2078
+ ],
2079
+ layout: {
2080
+ name: 'breadthfirst',
2081
+ directed: true,
2082
+ padding: 50,
2083
+ spacingFactor: 1.6,
2084
+ animate: true,
2085
+ animationDuration: 400,
2086
+ roots: elements.filter(function(e) { return e.data && e.data.id === '[START]'; }).length > 0
2087
+ ? ['[START]'] : undefined
2088
+ },
2089
+ minZoom: 0.15,
2090
+ maxZoom: 4,
2091
+ wheelSensitivity: 0.3
2092
+ });
2093
+
2094
+ // Click node → show detail
2095
+ this._cyProcessMap.on('tap', 'node', function(e) {
2096
+ var d = e.target.data().fullData;
2097
+ if (!d || d.isVirtual) return;
2098
+ var panel = document.getElementById('processMapDetailPanel');
2099
+ var title = document.getElementById('processMapDetailTitle');
2100
+ var body = document.getElementById('processMapDetailBody');
2101
+
2102
+ title.textContent = d.label;
2103
+ var html = '';
2104
+ html += self.detailRow('Occurrences', d.count);
2105
+ html += self.detailRow('Frequency', (d.frequency * 100).toFixed(1) + '% of traces');
2106
+ if (d.avgDuration > 0) html += self.detailRow('Avg Duration', self.computeDuration(0, d.avgDuration));
2107
+ html += self.detailRow('Failure Rate', (d.failRate * 100).toFixed(1) + '%');
2108
+ body.innerHTML = html;
2109
+ panel.classList.add('active');
2110
+ });
2111
+
2112
+ this._cyProcessMap.on('tap', function(e) {
2113
+ if (e.target === self._cyProcessMap) {
2114
+ document.getElementById('processMapDetailPanel').classList.remove('active');
2115
+ }
2116
+ });
2117
+
2118
+ // Close button
2119
+ var closeBtn = document.getElementById('processMapDetailClose');
2120
+ if (closeBtn) {
2121
+ closeBtn.onclick = function() {
2122
+ document.getElementById('processMapDetailPanel').classList.remove('active');
2123
+ };
2124
+ }
2125
+ }
1909
2126
  }
1910
2127
 
1911
2128
  // Initialize
@@ -1235,6 +1235,7 @@
1235
1235
  <div class="tab" data-tab="state">State Machine</div>
1236
1236
  <div class="tab" data-tab="summary">Summary</div>
1237
1237
  <div class="tab" data-tab="transcript">Transcript</div>
1238
+ <div class="tab" data-tab="processmap">Process Map</div>
1238
1239
  </div>
1239
1240
 
1240
1241
  <div class="toolbar" id="toolbar">
@@ -1332,6 +1333,24 @@
1332
1333
  </div>
1333
1334
  </div>
1334
1335
  </div>
1336
+
1337
+ <!-- Process Map tab -->
1338
+ <div class="tab-panel" id="panel-processmap">
1339
+ <div id="cyProcessMap" style="width:100%;height:100%;min-height:500px;">
1340
+ <div class="empty-state" id="processMapEmpty">
1341
+ <div class="empty-state-icon">&#9881;</div>
1342
+ <div class="empty-state-title">Process Map</div>
1343
+ <div class="empty-state-text">Select a trace to view the process mining graph for its agent.<br>Shows activity flows aggregated across all executions.</div>
1344
+ </div>
1345
+ </div>
1346
+ <div class="node-detail-panel" id="processMapDetailPanel">
1347
+ <div class="node-detail-header">
1348
+ <h4 id="processMapDetailTitle">Activity Details</h4>
1349
+ <button class="node-detail-close" id="processMapDetailClose">&times;</button>
1350
+ </div>
1351
+ <div class="node-detail-body" id="processMapDetailBody"></div>
1352
+ </div>
1353
+ </div>
1335
1354
  </div>
1336
1355
  </main>
1337
1356
  </div>
package/dist/server.cjs CHANGED
@@ -1701,6 +1701,98 @@ var DashboardServer = class {
1701
1701
  res.status(500).json({ error: "Failed to load statistics" });
1702
1702
  }
1703
1703
  });
1704
+ this.app.get("/api/agents/:agentId/process-graph", (req, res) => {
1705
+ try {
1706
+ const agentId = req.params.agentId;
1707
+ const traces = this.watcher.getTracesByAgent(agentId).map(serializeTrace);
1708
+ if (traces.length === 0) {
1709
+ return res.status(404).json({ error: "No traces for agent" });
1710
+ }
1711
+ const activityCounts = /* @__PURE__ */ new Map();
1712
+ const transitionCounts = /* @__PURE__ */ new Map();
1713
+ const activityDurations = /* @__PURE__ */ new Map();
1714
+ const activityStatuses = /* @__PURE__ */ new Map();
1715
+ let totalTraces = 0;
1716
+ for (const trace of traces) {
1717
+ totalTraces++;
1718
+ const activities = [];
1719
+ if (trace.sessionEvents && trace.sessionEvents.length > 0) {
1720
+ for (const evt of trace.sessionEvents) {
1721
+ const name = evt.toolName || evt.name || evt.type;
1722
+ if (!name) continue;
1723
+ activities.push({
1724
+ name,
1725
+ type: evt.type,
1726
+ status: evt.toolError ? "failed" : "completed",
1727
+ duration: evt.duration || 0
1728
+ });
1729
+ }
1730
+ } else {
1731
+ const nodes2 = trace.nodes || {};
1732
+ const sorted = Object.values(nodes2).sort((a, b) => (a.startTime || 0) - (b.startTime || 0));
1733
+ for (const node of sorted) {
1734
+ activities.push({
1735
+ name: node.name || node.type || node.id,
1736
+ type: node.type || "unknown",
1737
+ status: node.status || "completed",
1738
+ duration: (node.endTime || node.startTime || 0) - (node.startTime || 0)
1739
+ });
1740
+ }
1741
+ }
1742
+ const seq = ["[START]", ...activities.map((a) => a.name), "[END]"];
1743
+ for (let i = 0; i < seq.length; i++) {
1744
+ const act = seq[i];
1745
+ activityCounts.set(act, (activityCounts.get(act) || 0) + 1);
1746
+ if (i < seq.length - 1) {
1747
+ const key = act + " \u2192 " + seq[i + 1];
1748
+ transitionCounts.set(key, (transitionCounts.get(key) || 0) + 1);
1749
+ }
1750
+ }
1751
+ for (const act of activities) {
1752
+ if (act.duration > 0) {
1753
+ const durs = activityDurations.get(act.name) || [];
1754
+ durs.push(act.duration);
1755
+ activityDurations.set(act.name, durs);
1756
+ }
1757
+ const st = activityStatuses.get(act.name) || { ok: 0, fail: 0 };
1758
+ if (act.status === "failed") st.fail++;
1759
+ else st.ok++;
1760
+ activityStatuses.set(act.name, st);
1761
+ }
1762
+ }
1763
+ const nodes = Array.from(activityCounts.entries()).map(([name, count]) => {
1764
+ const durs = activityDurations.get(name) || [];
1765
+ const st = activityStatuses.get(name) || { ok: 0, fail: 0 };
1766
+ const avgDuration = durs.length > 0 ? durs.reduce((a, b) => a + b, 0) / durs.length : 0;
1767
+ return {
1768
+ id: name,
1769
+ label: name,
1770
+ count,
1771
+ frequency: count / totalTraces,
1772
+ avgDuration,
1773
+ failRate: st.ok + st.fail > 0 ? st.fail / (st.ok + st.fail) : 0,
1774
+ isVirtual: name === "[START]" || name === "[END]"
1775
+ };
1776
+ });
1777
+ const edges = Array.from(transitionCounts.entries()).map(([key, count]) => {
1778
+ const [source, target] = key.split(" \u2192 ");
1779
+ return { source, target, count, frequency: count / totalTraces };
1780
+ });
1781
+ const maxEdgeCount = Math.max(...edges.map((e) => e.count), 1);
1782
+ const maxNodeCount = Math.max(...nodes.filter((n) => !n.isVirtual).map((n) => n.count), 1);
1783
+ res.json({
1784
+ agentId,
1785
+ totalTraces,
1786
+ nodes,
1787
+ edges,
1788
+ maxEdgeCount,
1789
+ maxNodeCount
1790
+ });
1791
+ } catch (error) {
1792
+ console.error("Process graph error:", error);
1793
+ res.status(500).json({ error: "Failed to build process graph" });
1794
+ }
1795
+ });
1704
1796
  this.app.get("/api/stats/:agentId", (req, res) => {
1705
1797
  try {
1706
1798
  const agentStats = this.stats.getAgentStats(req.params.agentId);
package/dist/server.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DashboardServer
3
- } from "./chunk-25MUPUYY.js";
3
+ } from "./chunk-YDFLDRWO.js";
4
4
  export {
5
5
  DashboardServer
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentflow-dashboard",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Real-time monitoring dashboard for AgentFlow - Visualize agent execution graphs and performance",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -307,6 +307,12 @@ class AgentFlowDashboard {
307
307
  this.selectedTrace = trace;
308
308
  this.selectedTraceData = trace;
309
309
 
310
+ // Reset process map cache when agent changes
311
+ if (this._processMapAgent !== trace.agentId) {
312
+ this._processMapAgent = null;
313
+ if (this._cyProcessMap) { this._cyProcessMap.destroy(); this._cyProcessMap = null; }
314
+ }
315
+
310
316
  // Update sidebar selection
311
317
  document.querySelectorAll('.session-item').forEach(function(el) { el.classList.remove('active'); });
312
318
  var activeEl = document.querySelector('.session-item[data-filename="' + CSS.escape(filename) + '"]');
@@ -850,6 +856,7 @@ class AgentFlowDashboard {
850
856
  case 'state': this.renderStateMachine(); break;
851
857
  case 'summary': this.renderSummary(); break;
852
858
  case 'transcript': this.renderTranscript(); break;
859
+ case 'processmap': this.renderProcessMap(); break;
853
860
  }
854
861
  this.updateToolbarInfo();
855
862
  }
@@ -1906,6 +1913,216 @@ class AgentFlowDashboard {
1906
1913
 
1907
1914
  return 'other';
1908
1915
  }
1916
+
1917
+ // ---------------------------------------------------------------------------
1918
+ // Tab 8: Process Map (Process Mining Graph)
1919
+ // ---------------------------------------------------------------------------
1920
+ renderProcessMap() {
1921
+ var trace = this.selectedTraceData || this.selectedTrace;
1922
+ if (!trace || !trace.agentId) {
1923
+ document.getElementById('processMapEmpty').style.display = '';
1924
+ return;
1925
+ }
1926
+
1927
+ var agentId = trace.agentId;
1928
+ var self = this;
1929
+
1930
+ // Avoid re-fetching for same agent
1931
+ if (this._processMapAgent === agentId && this._cyProcessMap) return;
1932
+ this._processMapAgent = agentId;
1933
+
1934
+ document.getElementById('processMapEmpty').innerHTML =
1935
+ '<div class="empty-state-icon" style="animation:spin 1s linear infinite">&#9881;</div>' +
1936
+ '<div class="empty-state-text">Building process map for ' + escapeHtml(agentId) + '...</div>';
1937
+ document.getElementById('processMapEmpty').style.display = '';
1938
+
1939
+ fetch('/api/agents/' + encodeURIComponent(agentId) + '/process-graph')
1940
+ .then(function(r) { return r.json(); })
1941
+ .then(function(data) {
1942
+ if (data.error || !data.nodes || data.nodes.length === 0) {
1943
+ document.getElementById('processMapEmpty').innerHTML =
1944
+ '<div class="empty-state-icon">&#9881;</div>' +
1945
+ '<div class="empty-state-text">No process data for ' + escapeHtml(agentId) + '</div>';
1946
+ return;
1947
+ }
1948
+ document.getElementById('processMapEmpty').style.display = 'none';
1949
+ self._buildProcessMapGraph(data);
1950
+ })
1951
+ .catch(function() {
1952
+ document.getElementById('processMapEmpty').innerHTML =
1953
+ '<div class="empty-state-icon">&#9881;</div>' +
1954
+ '<div class="empty-state-text">Failed to load process map.</div>';
1955
+ });
1956
+ }
1957
+
1958
+ _buildProcessMapGraph(data) {
1959
+ if (this._cyProcessMap) { this._cyProcessMap.destroy(); this._cyProcessMap = null; }
1960
+
1961
+ var elements = [];
1962
+ var maxNode = data.maxNodeCount || 1;
1963
+ var maxEdge = data.maxEdgeCount || 1;
1964
+
1965
+ // Add nodes
1966
+ for (var i = 0; i < data.nodes.length; i++) {
1967
+ var node = data.nodes[i];
1968
+ // Skip very rare activities (< 2% frequency) to reduce clutter, but keep virtual nodes
1969
+ if (!node.isVirtual && node.frequency < 0.02 && data.nodes.length > 15) continue;
1970
+
1971
+ var size = node.isVirtual ? 30 : Math.max(25, Math.min(70, 25 + 45 * (node.count / maxNode)));
1972
+ var label = node.label;
1973
+ if (!node.isVirtual && node.count > 1) label += ' (' + node.count + ')';
1974
+
1975
+ elements.push({
1976
+ group: 'nodes',
1977
+ data: {
1978
+ id: node.id,
1979
+ label: label,
1980
+ count: node.count,
1981
+ frequency: node.frequency,
1982
+ avgDuration: node.avgDuration,
1983
+ failRate: node.failRate,
1984
+ isVirtual: node.isVirtual,
1985
+ size: size,
1986
+ fullData: node
1987
+ }
1988
+ });
1989
+ }
1990
+
1991
+ // Collect valid node IDs
1992
+ var validIds = new Set(elements.map(function(e) { return e.data.id; }));
1993
+
1994
+ // Add edges (only between valid nodes)
1995
+ for (var j = 0; j < data.edges.length; j++) {
1996
+ var edge = data.edges[j];
1997
+ if (!validIds.has(edge.source) || !validIds.has(edge.target)) continue;
1998
+ // Skip very rare transitions
1999
+ if (edge.frequency < 0.02 && data.edges.length > 30) continue;
2000
+
2001
+ var width = Math.max(1, Math.min(8, 1 + 7 * (edge.count / maxEdge)));
2002
+ var opacity = Math.max(0.3, Math.min(1.0, 0.3 + 0.7 * (edge.count / maxEdge)));
2003
+
2004
+ elements.push({
2005
+ group: 'edges',
2006
+ data: {
2007
+ id: 'pe-' + edge.source + '-' + edge.target,
2008
+ source: edge.source,
2009
+ target: edge.target,
2010
+ count: edge.count,
2011
+ frequency: edge.frequency,
2012
+ width: width,
2013
+ opacity: opacity,
2014
+ label: edge.count > 1 ? String(edge.count) : ''
2015
+ }
2016
+ });
2017
+ }
2018
+
2019
+ var container = document.getElementById('cyProcessMap');
2020
+ var self = this;
2021
+
2022
+ this._cyProcessMap = cytoscape({
2023
+ container: container,
2024
+ elements: elements,
2025
+ style: [
2026
+ {
2027
+ selector: 'node',
2028
+ style: {
2029
+ 'label': 'data(label)',
2030
+ 'width': 'data(size)',
2031
+ 'height': 'data(size)',
2032
+ 'font-size': '9px',
2033
+ 'text-valign': 'bottom',
2034
+ 'text-halign': 'center',
2035
+ 'text-margin-y': 6,
2036
+ 'color': '#c9d1d9',
2037
+ 'text-outline-color': '#0d1117',
2038
+ 'text-outline-width': 2,
2039
+ 'text-wrap': 'ellipsis',
2040
+ 'text-max-width': '100px',
2041
+ 'border-width': 2,
2042
+ 'border-color': '#30363d',
2043
+ 'background-color': '#3b82f6',
2044
+ 'shape': 'round-rectangle'
2045
+ }
2046
+ },
2047
+ // Virtual START/END nodes
2048
+ { selector: 'node[?isVirtual]', style: {
2049
+ 'background-color': '#6b7280', 'shape': 'ellipse', 'border-color': '#4b5563',
2050
+ 'font-size': '8px', 'font-weight': 'bold', 'text-valign': 'center', 'text-margin-y': 0
2051
+ }},
2052
+ // Color by fail rate: green → yellow → red
2053
+ { selector: 'node[failRate <= 0]', style: { 'background-color': '#10b981', 'border-color': '#2ea043' }},
2054
+ { selector: 'node[failRate > 0][failRate <= 0.1]', style: { 'background-color': '#22c55e', 'border-color': '#3fb950' }},
2055
+ { selector: 'node[failRate > 0.1][failRate <= 0.3]', style: { 'background-color': '#eab308', 'border-color': '#d29922' }},
2056
+ { selector: 'node[failRate > 0.3]', style: { 'background-color': '#ef4444', 'border-color': '#f85149' }},
2057
+ // Selected
2058
+ { selector: ':selected', style: { 'border-width': 4, 'border-color': '#f59e0b', 'overlay-opacity': 0.08 }},
2059
+ // Edges
2060
+ {
2061
+ selector: 'edge',
2062
+ style: {
2063
+ 'width': 'data(width)',
2064
+ 'opacity': 'data(opacity)',
2065
+ 'line-color': '#6b7280',
2066
+ 'target-arrow-color': '#6b7280',
2067
+ 'target-arrow-shape': 'triangle',
2068
+ 'curve-style': 'bezier',
2069
+ 'arrow-scale': 0.7,
2070
+ 'label': 'data(label)',
2071
+ 'font-size': '8px',
2072
+ 'color': '#8b949e',
2073
+ 'text-outline-color': '#0d1117',
2074
+ 'text-outline-width': 1.5,
2075
+ 'text-rotation': 'autorotate'
2076
+ }
2077
+ }
2078
+ ],
2079
+ layout: {
2080
+ name: 'breadthfirst',
2081
+ directed: true,
2082
+ padding: 50,
2083
+ spacingFactor: 1.6,
2084
+ animate: true,
2085
+ animationDuration: 400,
2086
+ roots: elements.filter(function(e) { return e.data && e.data.id === '[START]'; }).length > 0
2087
+ ? ['[START]'] : undefined
2088
+ },
2089
+ minZoom: 0.15,
2090
+ maxZoom: 4,
2091
+ wheelSensitivity: 0.3
2092
+ });
2093
+
2094
+ // Click node → show detail
2095
+ this._cyProcessMap.on('tap', 'node', function(e) {
2096
+ var d = e.target.data().fullData;
2097
+ if (!d || d.isVirtual) return;
2098
+ var panel = document.getElementById('processMapDetailPanel');
2099
+ var title = document.getElementById('processMapDetailTitle');
2100
+ var body = document.getElementById('processMapDetailBody');
2101
+
2102
+ title.textContent = d.label;
2103
+ var html = '';
2104
+ html += self.detailRow('Occurrences', d.count);
2105
+ html += self.detailRow('Frequency', (d.frequency * 100).toFixed(1) + '% of traces');
2106
+ if (d.avgDuration > 0) html += self.detailRow('Avg Duration', self.computeDuration(0, d.avgDuration));
2107
+ html += self.detailRow('Failure Rate', (d.failRate * 100).toFixed(1) + '%');
2108
+ body.innerHTML = html;
2109
+ panel.classList.add('active');
2110
+ });
2111
+
2112
+ this._cyProcessMap.on('tap', function(e) {
2113
+ if (e.target === self._cyProcessMap) {
2114
+ document.getElementById('processMapDetailPanel').classList.remove('active');
2115
+ }
2116
+ });
2117
+
2118
+ // Close button
2119
+ var closeBtn = document.getElementById('processMapDetailClose');
2120
+ if (closeBtn) {
2121
+ closeBtn.onclick = function() {
2122
+ document.getElementById('processMapDetailPanel').classList.remove('active');
2123
+ };
2124
+ }
2125
+ }
1909
2126
  }
1910
2127
 
1911
2128
  // Initialize
package/public/index.html CHANGED
@@ -1235,6 +1235,7 @@
1235
1235
  <div class="tab" data-tab="state">State Machine</div>
1236
1236
  <div class="tab" data-tab="summary">Summary</div>
1237
1237
  <div class="tab" data-tab="transcript">Transcript</div>
1238
+ <div class="tab" data-tab="processmap">Process Map</div>
1238
1239
  </div>
1239
1240
 
1240
1241
  <div class="toolbar" id="toolbar">
@@ -1332,6 +1333,24 @@
1332
1333
  </div>
1333
1334
  </div>
1334
1335
  </div>
1336
+
1337
+ <!-- Process Map tab -->
1338
+ <div class="tab-panel" id="panel-processmap">
1339
+ <div id="cyProcessMap" style="width:100%;height:100%;min-height:500px;">
1340
+ <div class="empty-state" id="processMapEmpty">
1341
+ <div class="empty-state-icon">&#9881;</div>
1342
+ <div class="empty-state-title">Process Map</div>
1343
+ <div class="empty-state-text">Select a trace to view the process mining graph for its agent.<br>Shows activity flows aggregated across all executions.</div>
1344
+ </div>
1345
+ </div>
1346
+ <div class="node-detail-panel" id="processMapDetailPanel">
1347
+ <div class="node-detail-header">
1348
+ <h4 id="processMapDetailTitle">Activity Details</h4>
1349
+ <button class="node-detail-close" id="processMapDetailClose">&times;</button>
1350
+ </div>
1351
+ <div class="node-detail-body" id="processMapDetailBody"></div>
1352
+ </div>
1353
+ </div>
1335
1354
  </div>
1336
1355
  </main>
1337
1356
  </div>