agent-nuvira 2.7.0 → 2.7.2

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 (114) hide show
  1. package/dist/agents/orchestrator.d.ts.map +1 -1
  2. package/dist/agents/orchestrator.js +17 -3
  3. package/dist/agents/orchestrator.js.map +1 -1
  4. package/dist/app.js +26 -0
  5. package/dist/cli/chat.d.ts.map +1 -1
  6. package/dist/cli/chat.js +108 -2
  7. package/dist/cli/chat.js.map +1 -1
  8. package/dist/config/auth.js +18 -0
  9. package/dist/config/jwt.js +24 -0
  10. package/dist/config/keys.js +14 -0
  11. package/dist/gateway/chat-store.d.ts +32 -5
  12. package/dist/gateway/chat-store.d.ts.map +1 -1
  13. package/dist/gateway/chat-store.js +52 -6
  14. package/dist/gateway/chat-store.js.map +1 -1
  15. package/dist/gateway/registry.d.ts.map +1 -1
  16. package/dist/gateway/registry.js +54 -10
  17. package/dist/gateway/registry.js.map +1 -1
  18. package/dist/inference/model-validator.d.ts.map +1 -1
  19. package/dist/inference/model-validator.js +11 -1
  20. package/dist/inference/model-validator.js.map +1 -1
  21. package/dist/inference/tool-call-utils.d.ts +31 -0
  22. package/dist/inference/tool-call-utils.d.ts.map +1 -1
  23. package/dist/inference/tool-call-utils.js +41 -0
  24. package/dist/inference/tool-call-utils.js.map +1 -1
  25. package/dist/learning/reasoning-trace.d.ts +5 -3
  26. package/dist/learning/reasoning-trace.d.ts.map +1 -1
  27. package/dist/learning/reasoning-trace.js +2 -2
  28. package/dist/learning/reasoning-trace.js.map +1 -1
  29. package/dist/middleware/auth.js +30 -0
  30. package/dist/nlu/conversation-gate.js +1 -1
  31. package/dist/nlu/conversation-gate.js.map +1 -1
  32. package/dist/nlu/entities.d.ts.map +1 -1
  33. package/dist/nlu/entities.js +12 -0
  34. package/dist/nlu/entities.js.map +1 -1
  35. package/dist/nlu/intent.d.ts.map +1 -1
  36. package/dist/nlu/intent.js +83 -15
  37. package/dist/nlu/intent.js.map +1 -1
  38. package/dist/passport.js +34 -0
  39. package/dist/routes/auth.js +42 -0
  40. package/dist/routes/user.js +31 -0
  41. package/dist/server.js +17 -0
  42. package/dist/tools/pipeline-tool.d.ts.map +1 -1
  43. package/dist/tools/pipeline-tool.js +14 -0
  44. package/dist/tools/pipeline-tool.js.map +1 -1
  45. package/dist/tools/release-sync.d.ts +52 -16
  46. package/dist/tools/release-sync.d.ts.map +1 -1
  47. package/dist/tools/release-sync.js +143 -79
  48. package/dist/tools/release-sync.js.map +1 -1
  49. package/dist/web-dashboard/src/App.js +85 -0
  50. package/dist/web-dashboard/src/admin-auth.test.js +186 -0
  51. package/dist/web-dashboard/src/ansi.js +23 -0
  52. package/dist/web-dashboard/src/ansi.test.js +31 -0
  53. package/dist/web-dashboard/src/api-admin-auth.test.js +172 -0
  54. package/dist/web-dashboard/src/api-admin.test.js +65 -0
  55. package/dist/web-dashboard/src/api-hub.test.js +117 -0
  56. package/dist/web-dashboard/src/api.js +1421 -0
  57. package/dist/web-dashboard/src/api.test.js +51 -0
  58. package/dist/web-dashboard/src/artifacts.js +128 -0
  59. package/dist/web-dashboard/src/artifacts.test.js +139 -0
  60. package/dist/web-dashboard/src/components/AdminPanel.js +567 -0
  61. package/dist/web-dashboard/src/components/AdminPanel.test.js +288 -0
  62. package/dist/web-dashboard/src/components/AgentHub.js +1580 -0
  63. package/dist/web-dashboard/src/components/AgentHub.test.js +343 -0
  64. package/dist/web-dashboard/src/components/BedrockOnboarding.js +320 -0
  65. package/dist/web-dashboard/src/components/BenchmarkCharts.js +228 -0
  66. package/dist/web-dashboard/src/components/ChatPage.js +1586 -0
  67. package/dist/web-dashboard/src/components/ChatPage.test.js +899 -0
  68. package/dist/web-dashboard/src/components/ContactsPage.js +141 -0
  69. package/dist/web-dashboard/src/components/CostDashboard.js +105 -0
  70. package/dist/web-dashboard/src/components/DAGView.js +477 -0
  71. package/dist/web-dashboard/src/components/DAGView.test.js +147 -0
  72. package/dist/web-dashboard/src/components/EnvVarEditor.js +138 -0
  73. package/dist/web-dashboard/src/components/EvalsPage.js +73 -0
  74. package/dist/web-dashboard/src/components/EvalsPage.test.js +120 -0
  75. package/dist/web-dashboard/src/components/ExecutionHistory.js +201 -0
  76. package/dist/web-dashboard/src/components/GatewayPage.js +40 -0
  77. package/dist/web-dashboard/src/components/GatewayPage.test.js +74 -0
  78. package/dist/web-dashboard/src/components/HealthPanel.js +68 -0
  79. package/dist/web-dashboard/src/components/HistoryBrowser.js +34 -0
  80. package/dist/web-dashboard/src/components/Layout.js +50 -0
  81. package/dist/web-dashboard/src/components/Markdown.js +152 -0
  82. package/dist/web-dashboard/src/components/Markdown.test.js +126 -0
  83. package/dist/web-dashboard/src/components/MarkdownZeroDep.js +237 -0
  84. package/dist/web-dashboard/src/components/MemoryPanel.js +81 -0
  85. package/dist/web-dashboard/src/components/ModelTimeline.js +298 -0
  86. package/dist/web-dashboard/src/components/ModelsPanel.js +1484 -0
  87. package/dist/web-dashboard/src/components/ModelsPanel.test.js +460 -0
  88. package/dist/web-dashboard/src/components/Overview.js +123 -0
  89. package/dist/web-dashboard/src/components/PhaseTimeline.js +359 -0
  90. package/dist/web-dashboard/src/components/PhaseTimeline.test.js +234 -0
  91. package/dist/web-dashboard/src/components/PlatformConfigSection.js +384 -0
  92. package/dist/web-dashboard/src/components/PlatformConfigSection.test.js +106 -0
  93. package/dist/web-dashboard/src/components/PlatformsPage.js +250 -0
  94. package/dist/web-dashboard/src/components/QuotaPanel.js +159 -0
  95. package/dist/web-dashboard/src/components/QuotaPanel.test.js +85 -0
  96. package/dist/web-dashboard/src/components/RequestsPanel.js +229 -0
  97. package/dist/web-dashboard/src/components/RequestsPanel.test.js +103 -0
  98. package/dist/web-dashboard/src/components/RoutingInsightsPanel.js +938 -0
  99. package/dist/web-dashboard/src/components/RoutingInsightsPanel.test.js +339 -0
  100. package/dist/web-dashboard/src/components/RoutingWalkthrough.js +408 -0
  101. package/dist/web-dashboard/src/components/RoutingWalkthrough.test.js +209 -0
  102. package/dist/web-dashboard/src/components/TaskConsole.js +119 -0
  103. package/dist/web-dashboard/src/components/TaskConsole.test.js +123 -0
  104. package/dist/web-dashboard/src/components/TasksPage.js +256 -0
  105. package/dist/web-dashboard/src/components/TasksPage.test.js +103 -0
  106. package/dist/web-dashboard/src/components/TracePanel.js +306 -0
  107. package/dist/web-dashboard/src/components/WhatsAppPanel.js +195 -0
  108. package/dist/web-dashboard/src/components/WhatsAppPanel.test.js +122 -0
  109. package/dist/web-dashboard/src/jsonOrNull.js +37 -0
  110. package/dist/web-dashboard/src/main.js +15 -0
  111. package/dist/web-dashboard/src/mask.js +11 -0
  112. package/dist/web-dashboard/vite.config.js +23 -0
  113. package/dist/web-dashboard/vitest.config.js +15 -0
  114. package/package.json +5 -3
@@ -0,0 +1,477 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.default = DAGView;
37
+ const react_1 = require("react");
38
+ const api_1 = require("../api");
39
+ const PhaseTimeline_1 = __importStar(require("./PhaseTimeline"));
40
+ // ─── Agent Visual Constants ─────────────────────────────────────────────────
41
+ const AGENT_ICONS = {
42
+ planner: '📋',
43
+ 'context-gatherer': '📂',
44
+ writer: '✏️',
45
+ reviewer: '👁️',
46
+ tester: '🧪',
47
+ debugger: '🐛',
48
+ runner: '▶️',
49
+ git: '🔀',
50
+ package: '📦',
51
+ 'github-release': '🏷️',
52
+ security: '🔒',
53
+ orchestrator: '🎯',
54
+ };
55
+ const AGENT_COLORS = {
56
+ planner: '#58a6ff',
57
+ 'context-gatherer': '#39d2c0',
58
+ writer: '#d29922',
59
+ reviewer: '#bc8cff',
60
+ tester: '#3fb950',
61
+ debugger: '#f85149',
62
+ runner: '#58a6ff',
63
+ git: '#f0883e',
64
+ package: '#db6d28',
65
+ 'github-release': '#3fb950',
66
+ security: '#f85149',
67
+ orchestrator: '#f0883e',
68
+ };
69
+ const STATUS_COLORS = {
70
+ pending: { bg: '#1a1f2e', stroke: '#30363d', text: '#6e7681' },
71
+ running: { bg: 'rgba(88, 166, 255, 0.12)', stroke: '#58a6ff', text: '#58a6ff' },
72
+ completed: { bg: 'rgba(63, 185, 80, 0.12)', stroke: '#3fb950', text: '#3fb950' },
73
+ failed: { bg: 'rgba(248, 81, 73, 0.12)', stroke: '#f85149', text: '#f85149' },
74
+ };
75
+ const STATUS_BADGES = {
76
+ pending: '⏳ Pending',
77
+ running: '▶️ Running',
78
+ completed: '✅ Done',
79
+ failed: '❌ Failed',
80
+ };
81
+ const COMPLEXITY_BADGES = {
82
+ trivial: { icon: '🟢', color: '#3fb950' },
83
+ simple: { icon: '🔵', color: '#58a6ff' },
84
+ moderate: { icon: '🟡', color: '#d29922' },
85
+ complex: { icon: '🟠', color: '#f0883e' },
86
+ critical: { icon: '🔴', color: '#f85149' },
87
+ };
88
+ const AGENT_LABELS = {
89
+ planner: 'Planner',
90
+ 'context-gatherer': 'Context',
91
+ writer: 'Writer',
92
+ reviewer: 'Reviewer',
93
+ tester: 'Tester',
94
+ debugger: 'Debugger',
95
+ runner: 'Runner',
96
+ git: 'Git',
97
+ package: 'Package',
98
+ 'github-release': 'Release',
99
+ security: 'Security',
100
+ orchestrator: 'Orchestrator',
101
+ };
102
+ function computeLayout(nodes, edges, nodeW = 160, nodeH = 68, gapX = 40, gapY = 24, padding = 40) {
103
+ if (nodes.length === 0) {
104
+ return { layoutNodes: [], svgW: 400, svgH: 200 };
105
+ }
106
+ // Assign steps based on topological order
107
+ const steps = new Map();
108
+ const visited = new Set();
109
+ function assignStep(id) {
110
+ if (steps.has(id))
111
+ return steps.get(id);
112
+ if (visited.has(id))
113
+ return 0; // cycle protection
114
+ visited.add(id);
115
+ const incoming = edges.filter((e) => e.to === id);
116
+ if (incoming.length === 0) {
117
+ steps.set(id, 0);
118
+ return 0;
119
+ }
120
+ const maxDepStep = Math.max(...incoming.map((e) => assignStep(e.from)));
121
+ const step = maxDepStep + 1;
122
+ steps.set(id, step);
123
+ return step;
124
+ }
125
+ for (const node of nodes)
126
+ assignStep(node.id);
127
+ // Group nodes by step
128
+ const stepGroups = new Map();
129
+ for (const node of nodes) {
130
+ const s = steps.get(node.id) ?? 0;
131
+ if (!stepGroups.has(s))
132
+ stepGroups.set(s, []);
133
+ stepGroups.get(s).push(node);
134
+ }
135
+ const maxStep = Math.max(...stepGroups.keys());
136
+ const maxNodesInStep = Math.max(...Array.from(stepGroups.values()).map((g) => g.length));
137
+ const svgW = (maxStep + 1) * (nodeW + gapX) + padding * 2 - gapX;
138
+ const svgH = Math.max(maxNodesInStep, 1) * (nodeH + gapY) + padding * 2 - gapY;
139
+ const layoutNodes = [];
140
+ for (const node of nodes) {
141
+ const step = steps.get(node.id) ?? 0;
142
+ const group = stepGroups.get(step);
143
+ const indexInStep = group.indexOf(node);
144
+ const totalInStep = group.length;
145
+ // Center the group vertically
146
+ const groupHeight = totalInStep * (nodeH + gapY) - gapY;
147
+ const startY = svgH / 2 - groupHeight / 2;
148
+ layoutNodes.push({
149
+ ...node,
150
+ x: padding + step * (nodeW + gapX),
151
+ y: startY + indexInStep * (nodeH + gapY),
152
+ w: nodeW,
153
+ h: nodeH,
154
+ step,
155
+ totalInStep,
156
+ indexInStep,
157
+ });
158
+ }
159
+ return { layoutNodes, svgW, svgH };
160
+ }
161
+ // ─── Time Formatting ────────────────────────────────────────────────────────
162
+ function formatDuration(start, end) {
163
+ if (!start)
164
+ return '--';
165
+ const ms = (end || Date.now()) - start;
166
+ if (ms < 1000)
167
+ return `${ms}ms`;
168
+ if (ms < 60000)
169
+ return `${(ms / 1000).toFixed(1)}s`;
170
+ return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
171
+ }
172
+ function formatTime(ts) {
173
+ if (!ts)
174
+ return '';
175
+ return new Date(ts).toLocaleTimeString('en-US', { minute: '2-digit', second: '2-digit' });
176
+ }
177
+ // ─── DAG Empty State ───────────────────────────────────────────────────────
178
+ function EmptyDAGState({ memoryTotal }) {
179
+ return (<>
180
+ <h2 className="section-title">🔀 Agent Execution DAG</h2>
181
+ <p className="section-description">
182
+ Live visualization of the agent execution pipeline. When an agent task runs, you'll see each step
183
+ appear here in real time as it moves through planning → context gathering → writing → review → testing.
184
+ </p>
185
+ <div className="dag-empty">
186
+ <div className="dag-empty-icon">🔀</div>
187
+ <h3>No Active Pipeline</h3>
188
+ <p>Run an agent task to see the execution pipeline appear here in real time.</p>
189
+ {memoryTotal !== undefined && memoryTotal > 0 && (<p className="dag-empty-hint">
190
+ <code>{memoryTotal}</code> past executions are stored in memory.
191
+ </p>)}
192
+ </div>
193
+ </>);
194
+ }
195
+ // ─── Main Component ─────────────────────────────────────────────────────────
196
+ function DAGView({ data }) {
197
+ const dag = data?.dag;
198
+ const memoryTotal = data?.memory?.total;
199
+ const [liveDAG, setLiveDAG] = (0, react_1.useState)(null);
200
+ const [selectedNode, setSelectedNode] = (0, react_1.useState)(null);
201
+ // Phase 4 — track the live DAG's engine context + loop-turn telemetry (the
202
+ // SSE 'dag' event carries them; a stale snapshot in data.dag is ignored in
203
+ // favor of the newest broadcast).
204
+ const [liveEngine, setLiveEngine] = (0, react_1.useState)(undefined);
205
+ const [liveEngineExplanation, setLiveEngineExplanation] = (0, react_1.useState)(undefined);
206
+ const [liveLoopTurn, setLiveLoopTurn] = (0, react_1.useState)(undefined);
207
+ // Subscribe to DAG events via the existing dashboard API connection
208
+ (0, react_1.useEffect)(() => {
209
+ const unsub = api_1.dashboardAPI.onDAGEvent((dagData) => {
210
+ setLiveDAG(dagData);
211
+ // Phase 4 — engine badge + loop-turn telemetry from the same broadcast.
212
+ setLiveEngine(dagData.engine);
213
+ setLiveEngineExplanation(dagData.engineExplanation);
214
+ setLiveLoopTurn(dagData.loopTurn);
215
+ });
216
+ return unsub;
217
+ }, []);
218
+ // Use data.dag from dashboard updates (init/refresh events include dag field)
219
+ const displayDAG = dag || liveDAG;
220
+ // Phase 4 — the engine context (live broadcast wins over a snapshot).
221
+ const engine = liveDAG ? liveEngine : (dag?.engine ?? liveEngine);
222
+ const engineExplanation = liveDAG ? liveEngineExplanation : (dag?.engineExplanation ?? liveEngineExplanation);
223
+ const loopTurn = liveDAG ? liveLoopTurn : (dag?.loopTurn ?? liveLoopTurn);
224
+ // Persisted + live pipeline runs powering the scrubbable Run Timeline.
225
+ const pipelineRuns = (0, react_1.useMemo)(() => (0, PhaseTimeline_1.collectPipelineRuns)(displayDAG, data?.pipelineRuns), [displayDAG, data?.pipelineRuns]);
226
+ // Scrubbing the timeline highlights the corresponding node in the DAG below.
227
+ const handleScrub = (phaseId) => {
228
+ setSelectedNode(phaseId);
229
+ };
230
+ // The Run Timeline must stay reachable for HISTORICAL runs even when no
231
+ // pipeline is live (the normal dashboard state) — only fall back to the
232
+ // empty state when there are neither runs nor a live DAG nor a loop turn
233
+ // (Phase 4: a loop turn renders its telemetry card even with no nodes).
234
+ const hasLiveDAG = !!(displayDAG && (displayDAG.nodes.length > 0 || displayDAG.active));
235
+ const hasRuns = pipelineRuns.length > 0;
236
+ const hasLoopTurn = !!loopTurn;
237
+ if (!hasLiveDAG && !hasRuns && !hasLoopTurn) {
238
+ return <EmptyDAGState memoryTotal={memoryTotal}/>;
239
+ }
240
+ const { nodes, edges, pipeline, active } = displayDAG ?? { nodes: [], edges: [], pipeline: null, active: false };
241
+ const { layoutNodes, svgW, svgH } = computeLayout(nodes, edges);
242
+ const runningCount = nodes.filter((n) => n.status === 'running').length;
243
+ const completedCount = nodes.filter((n) => n.status === 'completed').length;
244
+ const failedCount = nodes.filter((n) => n.status === 'failed').length;
245
+ const pendingCount = nodes.filter((n) => n.status === 'pending').length;
246
+ const totalCount = nodes.length;
247
+ return (<>
248
+ <h2 className="section-title">🔀 Agent Execution DAG</h2>
249
+
250
+ {/* Scrubbable Run Timeline — scrub to replay the run and highlight each
251
+ step. Shown whenever runs exist (live or historical), so past runs
252
+ stay reachable even with no active pipeline. */}
253
+ {hasRuns && (<div className="dag-timeline-section">
254
+ <h3 className="section-subtitle">⏱ Run Timeline <span className="timeline-subtitle">— scrub to replay · click a run to switch</span></h3>
255
+ <PhaseTimeline_1.default runs={pipelineRuns} onScrub={handleScrub}/>
256
+ </div>)}
257
+
258
+ {/* Phase 4 — Loop-turn telemetry card (the chat/execute loop engine):
259
+ per-turn tool-call stats, rendered even with no pipeline nodes. */}
260
+ {loopTurn && (<div className="dag-loop-turn-card" data-testid="dag-loop-turn-card">
261
+ <div className="dag-loop-turn-header">
262
+ <span className="dag-loop-turn-icon">🔁</span>
263
+ <span className="dag-loop-turn-title">Loop turn — {loopTurn.title || 'chat turn'}</span>
264
+ {loopTurn.active && <span className="dag-loop-turn-live">● recording</span>}
265
+ </div>
266
+ <div className="dag-loop-turn-meta">
267
+ <span className="dag-loop-turn-chip">
268
+ 🔧 {loopTurn.toolCallCount} tool call{loopTurn.toolCallCount === 1 ? '' : 's'}
269
+ </span>
270
+ {loopTurn.erroredToolCount > 0 && (<span className="dag-loop-turn-chip dag-loop-turn-chip-error">
271
+ ❌ {loopTurn.erroredToolCount} errored
272
+ </span>)}
273
+ {loopTurn.provider && (<span className="dag-loop-turn-chip dag-loop-turn-chip-dim">
274
+ {loopTurn.provider}{loopTurn.model ? ` · ${loopTurn.model}` : ''}
275
+ </span>)}
276
+ {typeof loopTurn.startedAt === 'number' && (<span className="dag-loop-turn-chip dag-loop-turn-chip-dim">
277
+ ⏱ {formatDuration(loopTurn.startedAt, loopTurn.endedAt)}
278
+ </span>)}
279
+ {loopTurn.bounded && <span className="dag-loop-turn-chip dag-loop-turn-chip-error">⛔ bounded</span>}
280
+ {loopTurn.generationFailed && (<span className="dag-loop-turn-chip dag-loop-turn-chip-error">💀 generation failed</span>)}
281
+ {loopTurn.cancelled && <span className="dag-loop-turn-chip dag-loop-turn-chip-dim">✖ cancelled</span>}
282
+ </div>
283
+ {loopTurn.toolCalls.length > 0 && (<div className="dag-loop-turn-tools">
284
+ {loopTurn.toolCalls.slice(-12).map((c, i) => (<span key={`${c.tool}-${i}`} className={`dag-loop-tool ${c.ok === false ? 'dag-loop-tool-error' : ''}`} title={c.error || `${c.tool}${c.durationMs !== undefined ? ` · ${c.durationMs}ms` : ''}`}>
285
+ {c.ok === false ? '❌' : '✓'} {c.tool}
286
+ </span>))}
287
+ </div>)}
288
+ </div>)}
289
+
290
+ {hasLiveDAG && (<>
291
+ {/* Pipeline Header */}
292
+ <div className={`dag-status-bar ${active ? 'active' : ''}`}>
293
+ <div className="dag-pipeline-name">
294
+ {active && <span className="dag-live-dot"/>}
295
+ {pipeline || 'Execution Pipeline'}
296
+ </div>
297
+ <div className="dag-pipeline-meta">
298
+ {active && <span className="dag-live-badge">LIVE</span>}
299
+ {/* Phase 4 — engine badge: which engine executed this run, with the
300
+ router's audit line as the tooltip. */}
301
+ {engine && (<span className={`dag-engine-badge dag-engine-${engine}`} title={engineExplanation || (engine === 'loop' ? 'Single agentic loop' : 'Orchestrator pipeline')} data-testid="dag-engine-badge">
302
+ {engine === 'loop' ? '🔁 loop engine' : '⬡ pipeline engine'}
303
+ </span>)}
304
+ <span className="dag-step-count">{totalCount} steps</span>
305
+ {runningCount > 0 && <span className="dag-running-badge">▶ {runningCount} running</span>}
306
+ {pendingCount > 0 && <span className="dag-pending-badge">⏳ {pendingCount} pending</span>}
307
+ <span className="dag-completed-badge">✅ {completedCount} done</span>
308
+ {failedCount > 0 && <span className="dag-failed-badge">❌ {failedCount} failed</span>}
309
+ </div>
310
+ </div>
311
+
312
+ {/* SVG DAG Visualization */}
313
+ <div className="dag-container">
314
+ <svg width={svgW} height={svgH} viewBox={`0 0 ${svgW} ${svgH}`}>
315
+ {/* Edge definitions */}
316
+ <defs>
317
+ {edges.map((edge) => (<marker key={`arrow-${edge.from}-${edge.to}`} id={`arrow-${edge.from}-${edge.to}`} markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
318
+ <polygon points="0 0, 8 3, 0 6" fill="#58a6ff"/>
319
+ </marker>))}
320
+ {/* Glow filter for running nodes */}
321
+ <filter id="glow">
322
+ <feGaussianBlur stdDeviation="3" result="coloredBlur"/>
323
+ <feMerge>
324
+ <feMergeNode in="coloredBlur"/>
325
+ <feMergeNode in="SourceGraphic"/>
326
+ </feMerge>
327
+ </filter>
328
+ </defs>
329
+
330
+ {/* Draw edges */}
331
+ {edges.map((edge) => {
332
+ const fromNode = layoutNodes.find((n) => n.id === edge.from);
333
+ const toNode = layoutNodes.find((n) => n.id === edge.to);
334
+ if (!fromNode || !toNode)
335
+ return null;
336
+ const startX = fromNode.x + fromNode.w;
337
+ const startY = fromNode.y + fromNode.h / 2;
338
+ const endX = toNode.x;
339
+ const endY = toNode.y + toNode.h / 2;
340
+ const midX = (startX + endX) / 2;
341
+ const toStatus = toNode.status;
342
+ const edgeColor = toStatus === 'failed' ? '#f85149'
343
+ : toStatus === 'running' ? '#58a6ff'
344
+ : toStatus === 'completed' ? '#3fb950'
345
+ : '#30363d';
346
+ return (<g key={`edge-${edge.from}-${edge.to}`}>
347
+ <path d={`M ${startX} ${startY} C ${midX} ${startY}, ${midX} ${endY}, ${endX} ${endY}`} fill="none" stroke={edgeColor} strokeWidth={toStatus === 'pending' ? 1.5 : 2.5} strokeOpacity={toStatus === 'pending' ? 0.3 : 0.8} markerEnd={`url(#arrow-${edge.from}-${edge.to})`} className="dag-edge"/>
348
+ </g>);
349
+ })}
350
+
351
+ {/* Draw nodes */}
352
+ {layoutNodes.map((node) => {
353
+ const colors = STATUS_COLORS[node.status];
354
+ const color = AGENT_COLORS[node.agentType] || '#58a6ff';
355
+ const icon = AGENT_ICONS[node.agentType] || '⚙️';
356
+ const label = AGENT_LABELS[node.agentType] || node.agentType;
357
+ const isSelected = selectedNode === node.id;
358
+ const isRunning = node.status === 'running';
359
+ return (<g key={`node-${node.id}`} onClick={() => setSelectedNode(isSelected ? null : node.id)} style={{ cursor: 'pointer' }} className="dag-node-group">
360
+ {/* Selection highlight */}
361
+ {isSelected && (<rect x={node.x - 4} y={node.y - 4} width={node.w + 8} height={node.h + 8} rx={10} ry={10} fill="none" stroke="#58a6ff" strokeWidth={2} strokeOpacity={0.5}/>)}
362
+
363
+ {/* Node background */}
364
+ <rect x={node.x} y={node.y} width={node.w} height={node.h} rx={8} ry={8} fill={colors.bg} stroke={colors.stroke} strokeWidth={isRunning ? 2.5 : 1.5} strokeOpacity={0.9} className={isRunning ? 'dag-node-running' : ''} filter={isRunning ? 'url(#glow)' : undefined}/>
365
+
366
+ {/* Icon + Agent Type */}
367
+ <text x={node.x + 10} y={node.y + 22} fill="#e6edf3" fontSize={10} fontWeight={600}>
368
+ {icon} {label}
369
+ </text>
370
+
371
+ {/* Status text */}
372
+ <text x={node.x + node.w - 10} y={node.y + 22} textAnchor="end" fill={colors.text} fontSize={9} fontWeight={500}>
373
+ {STATUS_BADGES[node.status]}
374
+ </text>
375
+
376
+ {/* Description */}
377
+ <text x={node.x + 10} y={node.y + 40} fill="#8b949e" fontSize={9}>
378
+ {node.description.length > 28
379
+ ? node.description.slice(0, 26) + '..'
380
+ : node.description}
381
+ </text>
382
+
383
+ {/* Complexity badge (per-subtask label) */}
384
+ {node.complexity && COMPLEXITY_BADGES[node.complexity] && (<text x={node.x + 10} y={node.y + 54} fill={COMPLEXITY_BADGES[node.complexity].color} fontSize={8} fontWeight={600}>
385
+ {COMPLEXITY_BADGES[node.complexity].icon} {node.complexity}
386
+ </text>)}
387
+
388
+ {/* Duration */}
389
+ <text x={node.x + 10} y={node.y + 56} fill="#6e7681" fontSize={8}>
390
+ {formatDuration(node.startedAt, node.completedAt)}
391
+ </text>
392
+
393
+ {/* Time */}
394
+ <text x={node.x + node.w - 10} y={node.y + 56} textAnchor="end" fill="#6e7681" fontSize={8}>
395
+ {formatTime(node.startedAt || node.completedAt)}
396
+ </text>
397
+
398
+ {/* Summary on completed nodes (if selected or always for failed) */}
399
+ {(node.status === 'failed' || isSelected) && node.summary && (<foreignObject x={Math.max(10, node.x - 80)} y={node.y + node.h + 6} width={node.w + 160} height={36}>
400
+ <div className="dag-node-summary" style={{
401
+ color: node.status === 'failed' ? '#f85149' : '#8b949e',
402
+ fontSize: '10px',
403
+ lineHeight: 1.4,
404
+ background: 'rgba(13,17,23,0.9)',
405
+ padding: '4px 8px',
406
+ borderRadius: '4px',
407
+ border: `1px solid ${node.status === 'failed' ? 'rgba(248,81,73,0.3)' : 'rgba(48,54,61,0.5)'}`,
408
+ maxHeight: '32px',
409
+ overflow: 'hidden',
410
+ textOverflow: 'ellipsis',
411
+ whiteSpace: 'nowrap',
412
+ }}>
413
+ {node.summary}
414
+ </div>
415
+ </foreignObject>)}
416
+ </g>);
417
+ })}
418
+ </svg>
419
+ </div>
420
+
421
+ {/* Node Status Table */}
422
+ <h3 className="section-subtitle">Step Details</h3>
423
+ <div className="dag-table-wrapper">
424
+ <table className="dag-table">
425
+ <thead>
426
+ <tr>
427
+ <th>Step</th>
428
+ <th>Agent</th>
429
+ <th>Complexity</th>
430
+ <th>Status</th>
431
+ <th>Duration</th>
432
+ <th>Time</th>
433
+ <th>Summary</th>
434
+ </tr>
435
+ </thead>
436
+ <tbody>
437
+ {layoutNodes.map((node) => (<tr key={node.id} className={`dag-row dag-row-${node.status}`}>
438
+ <td className="dag-cell-step">{node.step}</td>
439
+ <td className="dag-cell-agent">
440
+ <span className="dag-agent-dot" style={{ background: AGENT_COLORS[node.agentType] || '#58a6ff' }}/>
441
+ {AGENT_LABELS[node.agentType] || node.agentType}
442
+ </td>
443
+ <td className="dag-cell-complexity">
444
+ {node.complexity && COMPLEXITY_BADGES[node.complexity]
445
+ ? <span style={{ color: COMPLEXITY_BADGES[node.complexity].color, fontSize: 12 }}>
446
+ {COMPLEXITY_BADGES[node.complexity].icon} {node.complexity}
447
+ </span>
448
+ : <span style={{ color: '#6e7681', fontSize: 12 }}>—</span>}
449
+ </td>
450
+ <td className={`dag-cell-status dag-status-${node.status}`}>
451
+ {STATUS_BADGES[node.status]}
452
+ </td>
453
+ <td className="dag-cell-duration">{formatDuration(node.startedAt, node.completedAt)}</td>
454
+ <td className="dag-cell-time">{formatTime(node.startedAt || node.completedAt)}</td>
455
+ <td className="dag-cell-summary">{node.summary || node.description || '--'}</td>
456
+ </tr>))}
457
+ </tbody>
458
+ </table>
459
+ </div>
460
+
461
+ {/* Legend */}
462
+ <h3 className="section-subtitle">Agent Types</h3>
463
+ <div className="dag-legend">
464
+ {Object.entries(AGENT_ICONS).map(([type, icon]) => (<div className="legend-item" key={type}>
465
+ <span className="legend-dot" style={{ background: AGENT_COLORS[type] || '#58a6ff' }}/>
466
+ <span className="legend-icon">{icon}</span>
467
+ <span className="legend-label">{AGENT_LABELS[type] || type}</span>
468
+ </div>))}
469
+ <div className="legend-item">
470
+ <span className="legend-dot" style={{ background: 'transparent', border: '2px dashed #58a6ff' }}/>
471
+ <span className="legend-icon">⚡</span>
472
+ <span className="legend-label">Live node</span>
473
+ </div>
474
+ </div>
475
+ </>)}
476
+ </>);
477
+ }
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ /**
3
+ * Tests for DAGView's Run Timeline integration.
4
+ *
5
+ * Regression coverage for the reviewer-flagged bug: the scrubbable phase
6
+ * timeline must remain reachable for HISTORICAL runs even when no pipeline is
7
+ * live — previously the empty-state early return hid every persisted run the
8
+ * moment no DAG was active, defeating the whole point of persisting runs.
9
+ */
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ const vitest_1 = require("vitest");
15
+ const react_1 = require("@testing-library/react");
16
+ const DAGView_1 = __importDefault(require("./DAGView"));
17
+ const makeRun = () => ({
18
+ id: 'run-1',
19
+ goal: 'Refactor the router',
20
+ startedAt: 1_700_000_000_000,
21
+ endedAt: 1_700_000_030_000,
22
+ success: true,
23
+ totalDurationMs: 30_000,
24
+ phases: [
25
+ { id: 'a', agentType: 'planner', status: 'completed', description: 'Plan the refactor', durationMs: 10_000 },
26
+ { id: 'b', agentType: 'writer', status: 'completed', description: 'Apply the refactor', durationMs: 20_000 },
27
+ ],
28
+ });
29
+ const makeData = (partial) => ({ cost: {}, history: {}, benchmarks: {}, memory: {}, health: {}, serverTime: Date.now(), ...partial });
30
+ (0, vitest_1.describe)('DAGView Run Timeline', () => {
31
+ (0, vitest_1.it)('shows the timeline for historical runs even with no live DAG (regression)', () => {
32
+ (0, react_1.render)(<DAGView_1.default data={makeData({ pipelineRuns: { total: 1, runs: [makeRun()] } })}/>);
33
+ // Timeline + run goal render instead of the empty state
34
+ (0, vitest_1.expect)(react_1.screen.getByText(/run timeline/i)).toBeTruthy();
35
+ (0, vitest_1.expect)(react_1.screen.getByText('Refactor the router')).toBeTruthy();
36
+ (0, vitest_1.expect)(react_1.screen.getByText('Plan the refactor')).toBeTruthy();
37
+ (0, vitest_1.expect)(react_1.screen.queryByText('No Active Pipeline')).toBeNull();
38
+ });
39
+ (0, vitest_1.it)('falls back to the empty state when there are neither runs nor a live DAG', () => {
40
+ (0, react_1.render)(<DAGView_1.default data={makeData({})}/>);
41
+ (0, vitest_1.expect)(react_1.screen.getByText('No Active Pipeline')).toBeTruthy();
42
+ (0, vitest_1.expect)(react_1.screen.queryByText(/run timeline/i)).toBeNull();
43
+ });
44
+ (0, vitest_1.it)('renders both the live DAG and the timeline when a pipeline is active', () => {
45
+ (0, react_1.render)(<DAGView_1.default data={makeData({
46
+ dag: {
47
+ pipeline: 'Fix the auth bug',
48
+ nodes: [
49
+ { id: 'n1', agentType: 'planner', status: 'completed', description: 'Plan the fix', startedAt: 100, completedAt: 500 },
50
+ { id: 'n2', agentType: 'writer', status: 'running', description: 'Write the fix', startedAt: 500 },
51
+ ],
52
+ edges: [{ from: 'n1', to: 'n2' }],
53
+ timestamp: 600,
54
+ active: true,
55
+ },
56
+ pipelineRuns: { total: 0, runs: [] },
57
+ })}/>);
58
+ // Appears in the status bar AND the timeline run chip
59
+ (0, vitest_1.expect)(react_1.screen.getAllByText('Fix the auth bug').length).toBeGreaterThan(0);
60
+ // Live DAG status bar meta (badge includes the ▶ glyph)
61
+ (0, vitest_1.expect)(react_1.screen.getByText(/1 running/)).toBeTruthy();
62
+ // Timeline renders for the live run too
63
+ (0, vitest_1.expect)(react_1.screen.getByText(/run timeline/i)).toBeTruthy();
64
+ });
65
+ // ─── Phase 4 (AGENTIC_CAPABILITY_ASSESSMENT Addendum v4) — engine badge +
66
+ // loop-turn telemetry card.
67
+ (0, vitest_1.it)('Phase 4 — badges the pipeline engine in the status bar', () => {
68
+ (0, react_1.render)(<DAGView_1.default data={makeData({
69
+ dag: {
70
+ pipeline: 'CI run',
71
+ nodes: [{ id: 'n1', agentType: 'planner', status: 'completed', description: 'Plan' }],
72
+ edges: [],
73
+ timestamp: 1,
74
+ active: false,
75
+ engine: 'pipeline',
76
+ engineExplanation: 'CI pins the orchestrator',
77
+ },
78
+ pipelineRuns: { total: 0, runs: [] },
79
+ })}/>);
80
+ const badge = react_1.screen.getByTestId('dag-engine-badge');
81
+ (0, vitest_1.expect)(badge.textContent).toContain('pipeline');
82
+ });
83
+ (0, vitest_1.it)('Phase 4 — renders the loop-turn telemetry card even with no DAG nodes', () => {
84
+ (0, react_1.render)(<DAGView_1.default data={makeData({
85
+ dag: {
86
+ pipeline: null,
87
+ nodes: [],
88
+ edges: [],
89
+ timestamp: 1,
90
+ active: true,
91
+ engine: 'loop',
92
+ loopTurn: {
93
+ turnId: 'turn-1',
94
+ title: 'assess the code quality',
95
+ startedAt: Date.now() - 5_000,
96
+ active: true,
97
+ toolCallCount: 3,
98
+ erroredToolCount: 1,
99
+ provider: 'groq',
100
+ model: 'llama-3.3-70b',
101
+ toolCalls: [
102
+ { tool: 'read_file', ok: true, durationMs: 12 },
103
+ { tool: 'code_search', ok: true, durationMs: 30 },
104
+ { tool: 'edit_file', ok: false, error: 'match not found' },
105
+ ],
106
+ },
107
+ },
108
+ pipelineRuns: { total: 0, runs: [] },
109
+ })}/>);
110
+ // The turn card renders (not the empty state) — per-tool telemetry visible
111
+ (0, vitest_1.expect)(react_1.screen.getByTestId('dag-loop-turn-card')).toBeTruthy();
112
+ (0, vitest_1.expect)(react_1.screen.getByText(/assess the code quality/)).toBeTruthy();
113
+ (0, vitest_1.expect)(react_1.screen.getByText(/3 tool calls/)).toBeTruthy();
114
+ (0, vitest_1.expect)(react_1.screen.getByText(/1 errored/)).toBeTruthy();
115
+ (0, vitest_1.expect)(react_1.screen.getByText('✓ read_file')).toBeTruthy();
116
+ (0, vitest_1.expect)(react_1.screen.getByText('❌ edit_file')).toBeTruthy();
117
+ // Provider chip
118
+ (0, vitest_1.expect)(react_1.screen.getByText(/groq · llama-3.3-70b/)).toBeTruthy();
119
+ });
120
+ (0, vitest_1.it)('Phase 4 — the loop-turn card shows bounded / generation-failed chips', () => {
121
+ (0, react_1.render)(<DAGView_1.default data={makeData({
122
+ dag: {
123
+ pipeline: null,
124
+ nodes: [],
125
+ edges: [],
126
+ timestamp: 1,
127
+ active: false,
128
+ engine: 'loop',
129
+ loopTurn: {
130
+ turnId: 'turn-2',
131
+ title: 'huge refactor',
132
+ startedAt: Date.now() - 60_000,
133
+ endedAt: Date.now(),
134
+ active: false,
135
+ toolCallCount: 17,
136
+ erroredToolCount: 0,
137
+ bounded: true,
138
+ toolCalls: [],
139
+ },
140
+ },
141
+ pipelineRuns: { total: 0, runs: [] },
142
+ })}/>);
143
+ (0, vitest_1.expect)(react_1.screen.getByTestId('dag-loop-turn-card')).toBeTruthy();
144
+ (0, vitest_1.expect)(react_1.screen.getByText(/bounded/)).toBeTruthy();
145
+ (0, vitest_1.expect)(react_1.screen.getByText(/17 tool calls/)).toBeTruthy();
146
+ });
147
+ });