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.
- package/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/agents/orchestrator.js +17 -3
- package/dist/agents/orchestrator.js.map +1 -1
- package/dist/app.js +26 -0
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +108 -2
- package/dist/cli/chat.js.map +1 -1
- package/dist/config/auth.js +18 -0
- package/dist/config/jwt.js +24 -0
- package/dist/config/keys.js +14 -0
- package/dist/gateway/chat-store.d.ts +32 -5
- package/dist/gateway/chat-store.d.ts.map +1 -1
- package/dist/gateway/chat-store.js +52 -6
- package/dist/gateway/chat-store.js.map +1 -1
- package/dist/gateway/registry.d.ts.map +1 -1
- package/dist/gateway/registry.js +54 -10
- package/dist/gateway/registry.js.map +1 -1
- package/dist/inference/model-validator.d.ts.map +1 -1
- package/dist/inference/model-validator.js +11 -1
- package/dist/inference/model-validator.js.map +1 -1
- package/dist/inference/tool-call-utils.d.ts +31 -0
- package/dist/inference/tool-call-utils.d.ts.map +1 -1
- package/dist/inference/tool-call-utils.js +41 -0
- package/dist/inference/tool-call-utils.js.map +1 -1
- package/dist/learning/reasoning-trace.d.ts +5 -3
- package/dist/learning/reasoning-trace.d.ts.map +1 -1
- package/dist/learning/reasoning-trace.js +2 -2
- package/dist/learning/reasoning-trace.js.map +1 -1
- package/dist/middleware/auth.js +30 -0
- package/dist/nlu/conversation-gate.js +1 -1
- package/dist/nlu/conversation-gate.js.map +1 -1
- package/dist/nlu/entities.d.ts.map +1 -1
- package/dist/nlu/entities.js +12 -0
- package/dist/nlu/entities.js.map +1 -1
- package/dist/nlu/intent.d.ts.map +1 -1
- package/dist/nlu/intent.js +83 -15
- package/dist/nlu/intent.js.map +1 -1
- package/dist/passport.js +34 -0
- package/dist/routes/auth.js +42 -0
- package/dist/routes/user.js +31 -0
- package/dist/server.js +17 -0
- package/dist/tools/pipeline-tool.d.ts.map +1 -1
- package/dist/tools/pipeline-tool.js +14 -0
- package/dist/tools/pipeline-tool.js.map +1 -1
- package/dist/tools/release-sync.d.ts +52 -16
- package/dist/tools/release-sync.d.ts.map +1 -1
- package/dist/tools/release-sync.js +143 -79
- package/dist/tools/release-sync.js.map +1 -1
- package/dist/web-dashboard/src/App.js +85 -0
- package/dist/web-dashboard/src/admin-auth.test.js +186 -0
- package/dist/web-dashboard/src/ansi.js +23 -0
- package/dist/web-dashboard/src/ansi.test.js +31 -0
- package/dist/web-dashboard/src/api-admin-auth.test.js +172 -0
- package/dist/web-dashboard/src/api-admin.test.js +65 -0
- package/dist/web-dashboard/src/api-hub.test.js +117 -0
- package/dist/web-dashboard/src/api.js +1421 -0
- package/dist/web-dashboard/src/api.test.js +51 -0
- package/dist/web-dashboard/src/artifacts.js +128 -0
- package/dist/web-dashboard/src/artifacts.test.js +139 -0
- package/dist/web-dashboard/src/components/AdminPanel.js +567 -0
- package/dist/web-dashboard/src/components/AdminPanel.test.js +288 -0
- package/dist/web-dashboard/src/components/AgentHub.js +1580 -0
- package/dist/web-dashboard/src/components/AgentHub.test.js +343 -0
- package/dist/web-dashboard/src/components/BedrockOnboarding.js +320 -0
- package/dist/web-dashboard/src/components/BenchmarkCharts.js +228 -0
- package/dist/web-dashboard/src/components/ChatPage.js +1586 -0
- package/dist/web-dashboard/src/components/ChatPage.test.js +899 -0
- package/dist/web-dashboard/src/components/ContactsPage.js +141 -0
- package/dist/web-dashboard/src/components/CostDashboard.js +105 -0
- package/dist/web-dashboard/src/components/DAGView.js +477 -0
- package/dist/web-dashboard/src/components/DAGView.test.js +147 -0
- package/dist/web-dashboard/src/components/EnvVarEditor.js +138 -0
- package/dist/web-dashboard/src/components/EvalsPage.js +73 -0
- package/dist/web-dashboard/src/components/EvalsPage.test.js +120 -0
- package/dist/web-dashboard/src/components/ExecutionHistory.js +201 -0
- package/dist/web-dashboard/src/components/GatewayPage.js +40 -0
- package/dist/web-dashboard/src/components/GatewayPage.test.js +74 -0
- package/dist/web-dashboard/src/components/HealthPanel.js +68 -0
- package/dist/web-dashboard/src/components/HistoryBrowser.js +34 -0
- package/dist/web-dashboard/src/components/Layout.js +50 -0
- package/dist/web-dashboard/src/components/Markdown.js +152 -0
- package/dist/web-dashboard/src/components/Markdown.test.js +126 -0
- package/dist/web-dashboard/src/components/MarkdownZeroDep.js +237 -0
- package/dist/web-dashboard/src/components/MemoryPanel.js +81 -0
- package/dist/web-dashboard/src/components/ModelTimeline.js +298 -0
- package/dist/web-dashboard/src/components/ModelsPanel.js +1484 -0
- package/dist/web-dashboard/src/components/ModelsPanel.test.js +460 -0
- package/dist/web-dashboard/src/components/Overview.js +123 -0
- package/dist/web-dashboard/src/components/PhaseTimeline.js +359 -0
- package/dist/web-dashboard/src/components/PhaseTimeline.test.js +234 -0
- package/dist/web-dashboard/src/components/PlatformConfigSection.js +384 -0
- package/dist/web-dashboard/src/components/PlatformConfigSection.test.js +106 -0
- package/dist/web-dashboard/src/components/PlatformsPage.js +250 -0
- package/dist/web-dashboard/src/components/QuotaPanel.js +159 -0
- package/dist/web-dashboard/src/components/QuotaPanel.test.js +85 -0
- package/dist/web-dashboard/src/components/RequestsPanel.js +229 -0
- package/dist/web-dashboard/src/components/RequestsPanel.test.js +103 -0
- package/dist/web-dashboard/src/components/RoutingInsightsPanel.js +938 -0
- package/dist/web-dashboard/src/components/RoutingInsightsPanel.test.js +339 -0
- package/dist/web-dashboard/src/components/RoutingWalkthrough.js +408 -0
- package/dist/web-dashboard/src/components/RoutingWalkthrough.test.js +209 -0
- package/dist/web-dashboard/src/components/TaskConsole.js +119 -0
- package/dist/web-dashboard/src/components/TaskConsole.test.js +123 -0
- package/dist/web-dashboard/src/components/TasksPage.js +256 -0
- package/dist/web-dashboard/src/components/TasksPage.test.js +103 -0
- package/dist/web-dashboard/src/components/TracePanel.js +306 -0
- package/dist/web-dashboard/src/components/WhatsAppPanel.js +195 -0
- package/dist/web-dashboard/src/components/WhatsAppPanel.test.js +122 -0
- package/dist/web-dashboard/src/jsonOrNull.js +37 -0
- package/dist/web-dashboard/src/main.js +15 -0
- package/dist/web-dashboard/src/mask.js +11 -0
- package/dist/web-dashboard/vite.config.js +23 -0
- package/dist/web-dashboard/vitest.config.js +15 -0
- package/package.json +5 -3
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.dagToPipelineRun = dagToPipelineRun;
|
|
4
|
+
exports.collectPipelineRuns = collectPipelineRuns;
|
|
5
|
+
exports.default = PhaseTimeline;
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
// ─── Agent Visual Constants (mirrors DAGView so colors/legends stay consistent) ─
|
|
8
|
+
const AGENT_ICONS = {
|
|
9
|
+
planner: '📋',
|
|
10
|
+
'context-gatherer': '📂',
|
|
11
|
+
writer: '✏️',
|
|
12
|
+
reviewer: '👁️',
|
|
13
|
+
tester: '🧪',
|
|
14
|
+
debugger: '🐛',
|
|
15
|
+
runner: '▶️',
|
|
16
|
+
git: '🔀',
|
|
17
|
+
package: '📦',
|
|
18
|
+
'github-release': '🏷️',
|
|
19
|
+
security: '🔒',
|
|
20
|
+
orchestrator: '🎯',
|
|
21
|
+
};
|
|
22
|
+
const AGENT_COLORS = {
|
|
23
|
+
planner: '#58a6ff',
|
|
24
|
+
'context-gatherer': '#39d2c0',
|
|
25
|
+
writer: '#d29922',
|
|
26
|
+
reviewer: '#bc8cff',
|
|
27
|
+
tester: '#3fb950',
|
|
28
|
+
debugger: '#f85149',
|
|
29
|
+
runner: '#58a6ff',
|
|
30
|
+
git: '#f0883e',
|
|
31
|
+
package: '#db6d28',
|
|
32
|
+
'github-release': '#3fb950',
|
|
33
|
+
security: '#f85149',
|
|
34
|
+
orchestrator: '#f0883e',
|
|
35
|
+
};
|
|
36
|
+
const AGENT_LABELS = {
|
|
37
|
+
planner: 'Planner',
|
|
38
|
+
'context-gatherer': 'Context',
|
|
39
|
+
writer: 'Writer',
|
|
40
|
+
reviewer: 'Reviewer',
|
|
41
|
+
tester: 'Tester',
|
|
42
|
+
debugger: 'Debugger',
|
|
43
|
+
runner: 'Runner',
|
|
44
|
+
git: 'Git',
|
|
45
|
+
package: 'Package',
|
|
46
|
+
'github-release': 'Release',
|
|
47
|
+
security: 'Security',
|
|
48
|
+
orchestrator: 'Orchestrator',
|
|
49
|
+
};
|
|
50
|
+
const STATUS_BADGES = {
|
|
51
|
+
pending: { icon: '⏳', label: 'Pending' },
|
|
52
|
+
running: { icon: '▶️', label: 'Running' },
|
|
53
|
+
completed: { icon: '✅', label: 'Done' },
|
|
54
|
+
failed: { icon: '❌', label: 'Failed' },
|
|
55
|
+
};
|
|
56
|
+
const STATUS_COLORS = {
|
|
57
|
+
pending: '#6e7681',
|
|
58
|
+
running: '#58a6ff',
|
|
59
|
+
completed: '#3fb950',
|
|
60
|
+
failed: '#f85149',
|
|
61
|
+
};
|
|
62
|
+
// ─── Run derivation helpers (shared with DAGView) ────────────────────────────
|
|
63
|
+
/**
|
|
64
|
+
* Convert a live DAG into a PipelineRun shape so the active execution can be
|
|
65
|
+
* scrubbed in real time alongside historical runs. Returns null when there is
|
|
66
|
+
* nothing to show (no nodes).
|
|
67
|
+
*/
|
|
68
|
+
function dagToPipelineRun(dag) {
|
|
69
|
+
if (!dag || dag.nodes.length === 0)
|
|
70
|
+
return null;
|
|
71
|
+
const starts = dag.nodes.map((n) => n.startedAt || 0).filter(Boolean);
|
|
72
|
+
const ends = dag.nodes.map((n) => n.completedAt || 0).filter(Boolean);
|
|
73
|
+
const started = starts.length > 0 ? Math.min(...starts) : Date.now();
|
|
74
|
+
const ended = ends.length > 0 ? Math.max(...ends) : Date.now();
|
|
75
|
+
return {
|
|
76
|
+
id: `live-${started}`,
|
|
77
|
+
goal: dag.pipeline || 'Active execution',
|
|
78
|
+
startedAt: started,
|
|
79
|
+
endedAt: ends.length > 0 ? ended : undefined,
|
|
80
|
+
success: dag.nodes.length > 0 && dag.nodes.every((n) => n.status === 'completed'),
|
|
81
|
+
totalDurationMs: Math.max(0, ended - started),
|
|
82
|
+
phases: dag.nodes.map((n) => ({
|
|
83
|
+
id: n.id,
|
|
84
|
+
agentType: n.agentType,
|
|
85
|
+
status: n.status,
|
|
86
|
+
description: n.description,
|
|
87
|
+
complexity: n.complexity,
|
|
88
|
+
summary: n.summary,
|
|
89
|
+
startedAt: n.startedAt,
|
|
90
|
+
completedAt: n.completedAt,
|
|
91
|
+
durationMs: n.startedAt && n.completedAt ? n.completedAt - n.startedAt : undefined,
|
|
92
|
+
})),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Merge the live run (from the DAG) with persisted runs (from the server),
|
|
97
|
+
* de-duplicated by id, most relevant first (live, then newest).
|
|
98
|
+
*/
|
|
99
|
+
function collectPipelineRuns(dag, stored) {
|
|
100
|
+
const runs = [];
|
|
101
|
+
const seen = new Set();
|
|
102
|
+
const live = dagToPipelineRun(dag);
|
|
103
|
+
if (live) {
|
|
104
|
+
runs.push(live);
|
|
105
|
+
seen.add(live.id);
|
|
106
|
+
}
|
|
107
|
+
for (const run of stored?.runs || []) {
|
|
108
|
+
if (!seen.has(run.id)) {
|
|
109
|
+
runs.push(run);
|
|
110
|
+
seen.add(run.id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return runs;
|
|
114
|
+
}
|
|
115
|
+
// ─── Time formatting ─────────────────────────────────────────────────────────
|
|
116
|
+
function formatMs(ms) {
|
|
117
|
+
if (ms < 1000)
|
|
118
|
+
return `${Math.round(ms)}ms`;
|
|
119
|
+
if (ms < 60000)
|
|
120
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
121
|
+
return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`;
|
|
122
|
+
}
|
|
123
|
+
function formatClock(ts) {
|
|
124
|
+
if (!ts)
|
|
125
|
+
return '';
|
|
126
|
+
return new Date(ts).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
127
|
+
}
|
|
128
|
+
function layoutPhases(phases, totalMs) {
|
|
129
|
+
if (phases.length === 0)
|
|
130
|
+
return { laid: [], total: 0 };
|
|
131
|
+
const len = phases.length;
|
|
132
|
+
// Known durations stay proportional; unknown phases split an equal share of
|
|
133
|
+
// the remaining span so the track is always fully covered (never a gap that
|
|
134
|
+
// cannot be scrubbed — e.g. parallel nodes whose durations exceed the span).
|
|
135
|
+
const known = phases.map((p) => (p.durationMs && p.durationMs > 0 ? p.durationMs : 0));
|
|
136
|
+
const knownSum = known.reduce((a, d) => a + d, 0);
|
|
137
|
+
const raw = knownSum > 0
|
|
138
|
+
? phases.map((p, i) => (known[i] > 0 ? known[i] : knownSum / len))
|
|
139
|
+
: phases.map(() => 1);
|
|
140
|
+
// Normalize so widths always sum to exactly 1 — edge-to-edge, no drift.
|
|
141
|
+
const rawSum = raw.reduce((a, d) => a + d, 0) || 1;
|
|
142
|
+
let cursor = 0;
|
|
143
|
+
const laid = phases.map((p, i) => {
|
|
144
|
+
const width = raw[i] / rawSum;
|
|
145
|
+
const start = cursor;
|
|
146
|
+
cursor += width;
|
|
147
|
+
return { ...p, start, width, dur: raw[i] };
|
|
148
|
+
});
|
|
149
|
+
// Scrub range: the real run span when known, else the synthetic total.
|
|
150
|
+
const total = totalMs > 0 ? totalMs : rawSum;
|
|
151
|
+
return { laid, total };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A scrubbable phase timeline for a pipeline run — the phase-bars + draggable
|
|
155
|
+
* caret pattern inspired by llm-viz's PhaseTimeline, applied to agent pipeline
|
|
156
|
+
* executions (plan → gather → write → review → test).
|
|
157
|
+
*/
|
|
158
|
+
function PhaseTimeline({ runs, selectedRunId, onSelectRun, onScrub, }) {
|
|
159
|
+
const [activeId, setActiveId] = (0, react_1.useState)(null);
|
|
160
|
+
const run = runs.find((r) => r.id === (selectedRunId ?? activeId)) ?? runs[0] ?? null;
|
|
161
|
+
const [t, setT] = (0, react_1.useState)(0);
|
|
162
|
+
const [playing, setPlaying] = (0, react_1.useState)(false);
|
|
163
|
+
const trackRef = (0, react_1.useRef)(null);
|
|
164
|
+
const draggingRef = (0, react_1.useRef)(false);
|
|
165
|
+
// Keep the latest onScrub in a ref so the notification effect below never
|
|
166
|
+
// goes stale if a parent passes an inline callback.
|
|
167
|
+
const onScrubRef = (0, react_1.useRef)(onScrub);
|
|
168
|
+
(0, react_1.useEffect)(() => {
|
|
169
|
+
onScrubRef.current = onScrub;
|
|
170
|
+
}, [onScrub]);
|
|
171
|
+
// Switch back to a valid run when the available set changes.
|
|
172
|
+
(0, react_1.useEffect)(() => {
|
|
173
|
+
if (!run && runs.length > 0)
|
|
174
|
+
setActiveId(runs[0].id);
|
|
175
|
+
else if (run && !runs.some((r) => r.id === run.id))
|
|
176
|
+
setActiveId(runs[0]?.id ?? null);
|
|
177
|
+
}, [runs, run]);
|
|
178
|
+
const { laid, total } = (0, react_1.useMemo)(() => layoutPhases(run?.phases || [], run?.totalDurationMs || 0), [run]);
|
|
179
|
+
const frac = total > 0 ? Math.min(1, Math.max(0, t / total)) : 0;
|
|
180
|
+
const currentPhase = (0, react_1.useMemo)(() => {
|
|
181
|
+
if (total <= 0)
|
|
182
|
+
return null;
|
|
183
|
+
const at = frac * total;
|
|
184
|
+
return laid.find((p) => at >= p.start * total && at < (p.start + p.width) * total) || null;
|
|
185
|
+
}, [laid, frac, total]);
|
|
186
|
+
// Notify the parent (DAG highlight) whenever the scrubbed phase changes.
|
|
187
|
+
(0, react_1.useEffect)(() => {
|
|
188
|
+
onScrubRef.current?.(currentPhase?.id ?? null);
|
|
189
|
+
}, [currentPhase?.id]);
|
|
190
|
+
// Playback loop: sweep the full duration in ~2s. The end-of-sweep reset lives
|
|
191
|
+
// in a separate watcher — side effects must never run inside a state updater.
|
|
192
|
+
(0, react_1.useEffect)(() => {
|
|
193
|
+
if (!playing)
|
|
194
|
+
return;
|
|
195
|
+
const interval = setInterval(() => {
|
|
196
|
+
setT((prev) => prev + (total / 2000) * 50);
|
|
197
|
+
}, 50);
|
|
198
|
+
return () => clearInterval(interval);
|
|
199
|
+
}, [playing, total]);
|
|
200
|
+
// Reach the end → stop and rewind to the start (sweep completes, then rests).
|
|
201
|
+
(0, react_1.useEffect)(() => {
|
|
202
|
+
if (playing && total > 0 && t >= total) {
|
|
203
|
+
setPlaying(false);
|
|
204
|
+
setT(0);
|
|
205
|
+
}
|
|
206
|
+
}, [playing, total, t]);
|
|
207
|
+
const setFromClientX = (0, react_1.useCallback)((clientX) => {
|
|
208
|
+
const el = trackRef.current;
|
|
209
|
+
if (!el || total <= 0)
|
|
210
|
+
return;
|
|
211
|
+
const rect = el.getBoundingClientRect();
|
|
212
|
+
const width = rect.width || 1;
|
|
213
|
+
const frac = Math.min(1, Math.max(0, (clientX - rect.left) / width));
|
|
214
|
+
setT(frac * total);
|
|
215
|
+
}, [total]);
|
|
216
|
+
const handlePointerDown = (e) => {
|
|
217
|
+
draggingRef.current = true;
|
|
218
|
+
setPlaying(false);
|
|
219
|
+
setFromClientX(e.clientX);
|
|
220
|
+
};
|
|
221
|
+
// Register the window listeners once; the drag flag lives in a ref so the
|
|
222
|
+
// listeners don't churn on every caret tick during playback.
|
|
223
|
+
(0, react_1.useEffect)(() => {
|
|
224
|
+
const onMove = (e) => {
|
|
225
|
+
if (draggingRef.current)
|
|
226
|
+
setFromClientX(e.clientX);
|
|
227
|
+
};
|
|
228
|
+
const onUp = () => {
|
|
229
|
+
draggingRef.current = false;
|
|
230
|
+
};
|
|
231
|
+
window.addEventListener('pointermove', onMove);
|
|
232
|
+
window.addEventListener('pointerup', onUp);
|
|
233
|
+
return () => {
|
|
234
|
+
window.removeEventListener('pointermove', onMove);
|
|
235
|
+
window.removeEventListener('pointerup', onUp);
|
|
236
|
+
};
|
|
237
|
+
}, [setFromClientX]);
|
|
238
|
+
if (!run) {
|
|
239
|
+
return (<div className="phase-timeline phase-timeline-empty">
|
|
240
|
+
<p>No pipeline runs yet. Run an agent task to see its phase timeline here.</p>
|
|
241
|
+
</div>);
|
|
242
|
+
}
|
|
243
|
+
const isLive = run.id.startsWith('live-');
|
|
244
|
+
const completedCount = run.phases.filter((p) => p.status === 'completed').length;
|
|
245
|
+
const failedCount = run.phases.filter((p) => p.status === 'failed').length;
|
|
246
|
+
return (<div className="phase-timeline">
|
|
247
|
+
{/* Run selector + meta */}
|
|
248
|
+
<div className="phase-timeline-header">
|
|
249
|
+
<div className="phase-timeline-runs">
|
|
250
|
+
{runs.map((r) => (<button key={r.id} className={`phase-run-chip ${r.id === run.id ? 'active' : ''} ${r.id.startsWith('live-') ? 'live' : ''}`} onClick={() => {
|
|
251
|
+
setActiveId(r.id);
|
|
252
|
+
setT(0);
|
|
253
|
+
setPlaying(false);
|
|
254
|
+
onSelectRun?.(r.id);
|
|
255
|
+
}} title={r.goal}>
|
|
256
|
+
{r.id.startsWith('live-') && <span className="dag-live-dot"/>}
|
|
257
|
+
{/* Phase 4 — engine tag on every run chip (loop turns persist
|
|
258
|
+
engine 'loop'; pipeline runs 'pipeline'). */}
|
|
259
|
+
{r.engine && (<span className={`phase-run-chip-engine phase-run-chip-engine-${r.engine}`}>
|
|
260
|
+
{r.engine === 'loop' ? '🔁' : '⬡'}
|
|
261
|
+
</span>)}
|
|
262
|
+
{r.goal.length > 32 ? r.goal.slice(0, 30) + '…' : r.goal}
|
|
263
|
+
<span className="phase-run-chip-time">{formatClock(r.startedAt)}</span>
|
|
264
|
+
</button>))}
|
|
265
|
+
</div>
|
|
266
|
+
<div className="phase-timeline-meta">
|
|
267
|
+
{isLive && <span className="dag-live-badge">LIVE</span>}
|
|
268
|
+
{/* Phase 4 — the selected run's engine + loop-turn telemetry. */}
|
|
269
|
+
{run.engine && (<span className={`dag-engine-badge dag-engine-${run.engine}`} title={run.engine === 'loop' ? 'Loop turn (per-tool telemetry below)' : 'Orchestrator pipeline'}>
|
|
270
|
+
{run.engine === 'loop' ? '🔁 loop' : '⬡ pipeline'}
|
|
271
|
+
</span>)}
|
|
272
|
+
<span className="phase-meta-item">{run.phases.length} steps</span>
|
|
273
|
+
<span className="phase-meta-item phase-meta-ok">✅ {completedCount}</span>
|
|
274
|
+
{failedCount > 0 && <span className="phase-meta-item phase-meta-bad">❌ {failedCount}</span>}
|
|
275
|
+
{run.turnTelemetry && (<span className="phase-meta-item" title={`tool calls: ${run.turnTelemetry.toolCallCount}, errored: ${run.turnTelemetry.erroredToolCount}${run.turnTelemetry.bounded ? ', bounded' : ''}${run.turnTelemetry.provider ? `, ${run.turnTelemetry.provider}${run.turnTelemetry.model ? `/${run.turnTelemetry.model}` : ''}` : ''}`}>
|
|
276
|
+
🔧 {run.turnTelemetry.toolCallCount}
|
|
277
|
+
{run.turnTelemetry.erroredToolCount > 0 && (<span className="phase-meta-bad"> ({run.turnTelemetry.erroredToolCount} err)</span>)}
|
|
278
|
+
</span>)}
|
|
279
|
+
<span className="phase-meta-item">⏱ {formatMs(total)}</span>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
{/* Scrub controls */}
|
|
284
|
+
<div className="phase-timeline-controls">
|
|
285
|
+
<button className={`phase-play-btn ${playing ? 'paused' : ''}`} onClick={() => {
|
|
286
|
+
if (playing) {
|
|
287
|
+
setPlaying(false);
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
if (t >= total)
|
|
291
|
+
setT(0);
|
|
292
|
+
setPlaying(true);
|
|
293
|
+
}
|
|
294
|
+
}} aria-label={playing ? 'Pause scrub' : 'Play scrub'}>
|
|
295
|
+
{playing ? '⏸ Pause' : '▶ Play'}
|
|
296
|
+
</button>
|
|
297
|
+
<span className="phase-time-label">{formatMs(t)} / {formatMs(total)}</span>
|
|
298
|
+
<input className="phase-range" type="range" min={0} max={total || 1} step={1} value={Math.min(t, total || 1)} onChange={(e) => { setPlaying(false); setT(Number(e.target.value)); }} aria-label="Scrub pipeline timeline"/>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
{/* Phase track + caret */}
|
|
302
|
+
<div className="phase-track-wrap">
|
|
303
|
+
<div ref={trackRef} className="phase-track" onPointerDown={handlePointerDown} style={{ cursor: 'grab' }}>
|
|
304
|
+
{laid.map((p) => {
|
|
305
|
+
const color = AGENT_COLORS[p.agentType] || '#58a6ff';
|
|
306
|
+
const statusColor = STATUS_COLORS[p.status];
|
|
307
|
+
const isActive = currentPhase?.id === p.id;
|
|
308
|
+
return (<button key={p.id} className={`phase-block phase-block-${p.status} ${isActive ? 'active' : ''}`} style={{
|
|
309
|
+
left: `${p.start * 100}%`,
|
|
310
|
+
width: `${Math.max(p.width * 100, 2)}%`,
|
|
311
|
+
background: `linear-gradient(180deg, ${color}cc, ${color}66)`,
|
|
312
|
+
boxShadow: isActive ? `0 0 0 2px ${statusColor}` : undefined,
|
|
313
|
+
}} onClick={(e) => {
|
|
314
|
+
e.stopPropagation();
|
|
315
|
+
setPlaying(false);
|
|
316
|
+
setT((p.start + p.width / 2) * total);
|
|
317
|
+
}} title={`${AGENT_LABELS[p.agentType] || p.agentType}: ${p.description}`}>
|
|
318
|
+
<span className="phase-block-icon">{AGENT_ICONS[p.agentType] || '⚙️'}</span>
|
|
319
|
+
<span className="phase-block-label">{AGENT_LABELS[p.agentType] || p.agentType}</span>
|
|
320
|
+
</button>);
|
|
321
|
+
})}
|
|
322
|
+
{/* Caret */}
|
|
323
|
+
<div className="phase-caret" style={{ left: `${frac * 100}%` }} onPointerDown={(e) => { e.stopPropagation(); handlePointerDown(e); }} title={`${formatMs(t)} elapsed`}/>
|
|
324
|
+
</div>
|
|
325
|
+
{/* Ticks */}
|
|
326
|
+
<div className="phase-ticks">
|
|
327
|
+
<span>0</span>
|
|
328
|
+
<span>{formatMs(Math.round(total / 2))}</span>
|
|
329
|
+
<span>{formatMs(total)}</span>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
|
|
333
|
+
{/* Detail panel: the phase under the caret */}
|
|
334
|
+
<div className="phase-detail">
|
|
335
|
+
{currentPhase ? (<>
|
|
336
|
+
<div className="phase-detail-title">
|
|
337
|
+
<span className="phase-detail-icon">{AGENT_ICONS[currentPhase.agentType] || '⚙️'}</span>
|
|
338
|
+
<span className="phase-detail-agent" style={{ color: AGENT_COLORS[currentPhase.agentType] || '#58a6ff' }}>
|
|
339
|
+
{AGENT_LABELS[currentPhase.agentType] || currentPhase.agentType}
|
|
340
|
+
</span>
|
|
341
|
+
<span className={`phase-status-badge phase-status-${currentPhase.status}`}>
|
|
342
|
+
{STATUS_BADGES[currentPhase.status].icon} {STATUS_BADGES[currentPhase.status].label}
|
|
343
|
+
</span>
|
|
344
|
+
</div>
|
|
345
|
+
<p className="phase-detail-desc">{currentPhase.description}</p>
|
|
346
|
+
<div className="phase-detail-meta">
|
|
347
|
+
{/* Known duration → exact; reconstructed (unknown) → approximate */}
|
|
348
|
+
<span>⏱ {currentPhase.durationMs ? formatMs(currentPhase.dur) : `~${formatMs(currentPhase.dur)}`}</span>
|
|
349
|
+
{currentPhase.startedAt ? <span>🕐 {formatClock(currentPhase.startedAt)}</span> : null}
|
|
350
|
+
{currentPhase.complexity ? <span>🎯 {currentPhase.complexity}</span> : null}
|
|
351
|
+
</div>
|
|
352
|
+
{currentPhase.summary && <p className="phase-detail-summary">{currentPhase.summary}</p>}
|
|
353
|
+
</>) : (<div className="phase-detail-gap">
|
|
354
|
+
<span className="phase-detail-icon">⏳</span>
|
|
355
|
+
<span>Between steps — waiting for the next agent.</span>
|
|
356
|
+
</div>)}
|
|
357
|
+
</div>
|
|
358
|
+
</div>);
|
|
359
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for the scrubbable PhaseTimeline component (llm-viz-inspired).
|
|
4
|
+
*
|
|
5
|
+
* Covers the empty state, run rendering (phase blocks + meta), scrub-to-phase
|
|
6
|
+
* via block click (the jsdom-safe path — no getBoundingClientRect needed),
|
|
7
|
+
* run switching, play/pause toggling, and the dagToPipelineRun /
|
|
8
|
+
* collectPipelineRuns helpers that derive runs from the live DAG + persisted
|
|
9
|
+
* pipeline-runs.
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
const vitest_1 = require("vitest");
|
|
46
|
+
const react_1 = require("@testing-library/react");
|
|
47
|
+
const PhaseTimeline_1 = __importStar(require("./PhaseTimeline"));
|
|
48
|
+
// ─── Fixtures ───────────────────────────────────────────────────────────────
|
|
49
|
+
const makeRun = (overrides = {}) => ({
|
|
50
|
+
id: 'run-1',
|
|
51
|
+
goal: 'Implement JWT auth middleware',
|
|
52
|
+
startedAt: 1_700_000_000_000,
|
|
53
|
+
endedAt: 1_700_000_060_000,
|
|
54
|
+
success: true,
|
|
55
|
+
totalDurationMs: 60_000,
|
|
56
|
+
phases: [
|
|
57
|
+
{
|
|
58
|
+
id: 'p1',
|
|
59
|
+
agentType: 'planner',
|
|
60
|
+
status: 'completed',
|
|
61
|
+
description: 'Break the goal into tasks',
|
|
62
|
+
complexity: 'moderate',
|
|
63
|
+
summary: 'Planned 4 steps',
|
|
64
|
+
startedAt: 1_700_000_000_000,
|
|
65
|
+
completedAt: 1_700_000_010_000,
|
|
66
|
+
durationMs: 10_000,
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'p2',
|
|
70
|
+
agentType: 'writer',
|
|
71
|
+
status: 'completed',
|
|
72
|
+
description: 'Write the middleware',
|
|
73
|
+
complexity: 'simple',
|
|
74
|
+
summary: 'Wrote src/middleware/auth.ts',
|
|
75
|
+
startedAt: 1_700_000_010_000,
|
|
76
|
+
completedAt: 1_700_000_040_000,
|
|
77
|
+
durationMs: 30_000,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'p3',
|
|
81
|
+
agentType: 'tester',
|
|
82
|
+
status: 'failed',
|
|
83
|
+
description: 'Run the test suite',
|
|
84
|
+
complexity: 'simple',
|
|
85
|
+
startedAt: 1_700_000_040_000,
|
|
86
|
+
completedAt: 1_700_000_060_000,
|
|
87
|
+
durationMs: 20_000,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
...overrides,
|
|
91
|
+
});
|
|
92
|
+
// ─── Component behavior ─────────────────────────────────────────────────────
|
|
93
|
+
(0, vitest_1.describe)('PhaseTimeline — Phase 4 engine chips', () => {
|
|
94
|
+
(0, vitest_1.it)('badges a loop-engine run chip with the loop glyph and the telemetry meta', () => {
|
|
95
|
+
const run = makeRun({
|
|
96
|
+
id: 'loop-turn-1',
|
|
97
|
+
goal: 'assess the code quality',
|
|
98
|
+
engine: 'loop',
|
|
99
|
+
turnTelemetry: { toolCallCount: 4, erroredToolCount: 1, provider: 'groq', model: 'llama-3.3-70b' },
|
|
100
|
+
});
|
|
101
|
+
(0, react_1.render)(<PhaseTimeline_1.default runs={[run]}/>);
|
|
102
|
+
// Engine badge in the meta row
|
|
103
|
+
(0, vitest_1.expect)(react_1.screen.getByText('🔁 loop')).toBeTruthy();
|
|
104
|
+
// Per-turn telemetry chip (🔧 count + errored parenthetical)
|
|
105
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/🔧 4/)).toBeTruthy();
|
|
106
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/\(1 err\)/)).toBeTruthy();
|
|
107
|
+
});
|
|
108
|
+
(0, vitest_1.it)('badges a pipeline run with the pipeline glyph and no telemetry chip', () => {
|
|
109
|
+
const run = makeRun({ id: 'pipe-1', engine: 'pipeline' });
|
|
110
|
+
(0, react_1.render)(<PhaseTimeline_1.default runs={[run]}/>);
|
|
111
|
+
(0, vitest_1.expect)(react_1.screen.getByText('⬡ pipeline')).toBeTruthy();
|
|
112
|
+
(0, vitest_1.expect)(react_1.screen.queryByText(/🔧/)).toBeNull();
|
|
113
|
+
});
|
|
114
|
+
(0, vitest_1.it)('shows no engine badge for legacy runs without one', () => {
|
|
115
|
+
(0, react_1.render)(<PhaseTimeline_1.default runs={[makeRun()]}/>);
|
|
116
|
+
(0, vitest_1.expect)(react_1.screen.queryByText(/🔁 loop/)).toBeNull();
|
|
117
|
+
(0, vitest_1.expect)(react_1.screen.queryByText(/⬡ pipeline/)).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
(0, vitest_1.describe)('PhaseTimeline', () => {
|
|
121
|
+
(0, vitest_1.it)('renders the empty state when there are no runs', () => {
|
|
122
|
+
(0, react_1.render)(<PhaseTimeline_1.default runs={[]}/>);
|
|
123
|
+
(0, vitest_1.expect)(react_1.screen.getByText(/no pipeline runs yet/i)).toBeTruthy();
|
|
124
|
+
});
|
|
125
|
+
(0, vitest_1.it)('renders phase blocks, step meta, and the run goal', () => {
|
|
126
|
+
(0, react_1.render)(<PhaseTimeline_1.default runs={[makeRun()]}/>);
|
|
127
|
+
// Run chip with goal
|
|
128
|
+
(0, vitest_1.expect)(react_1.screen.getByText('Implement JWT auth middleware')).toBeTruthy();
|
|
129
|
+
// Meta: 3 steps, 2 done, 1 failed, total duration
|
|
130
|
+
(0, vitest_1.expect)(react_1.screen.getByText('3 steps')).toBeTruthy();
|
|
131
|
+
(0, vitest_1.expect)(react_1.screen.getByText('✅ 2')).toBeTruthy();
|
|
132
|
+
(0, vitest_1.expect)(react_1.screen.getByText('❌ 1')).toBeTruthy();
|
|
133
|
+
// Phase labels on blocks (also appear in the detail panel — use *AllBy*)
|
|
134
|
+
(0, vitest_1.expect)(react_1.screen.getAllByText('Planner').length).toBeGreaterThan(0);
|
|
135
|
+
(0, vitest_1.expect)(react_1.screen.getAllByText('Writer').length).toBeGreaterThan(0);
|
|
136
|
+
(0, vitest_1.expect)(react_1.screen.getAllByText('Tester').length).toBeGreaterThan(0);
|
|
137
|
+
// Default detail panel shows the first phase under the caret
|
|
138
|
+
(0, vitest_1.expect)(react_1.screen.getByText('Break the goal into tasks')).toBeTruthy();
|
|
139
|
+
});
|
|
140
|
+
(0, vitest_1.it)('scrubs to a phase on block click and notifies the parent via onScrub', () => {
|
|
141
|
+
const onScrub = vitest_1.vi.fn();
|
|
142
|
+
(0, react_1.render)(<PhaseTimeline_1.default runs={[makeRun()]} onScrub={onScrub}/>);
|
|
143
|
+
const testerBlock = react_1.screen.getByText('Tester').closest('button');
|
|
144
|
+
react_1.fireEvent.click(testerBlock);
|
|
145
|
+
(0, vitest_1.expect)(onScrub).toHaveBeenCalledWith('p3');
|
|
146
|
+
// Detail panel now describes the scrubbed phase
|
|
147
|
+
(0, vitest_1.expect)(react_1.screen.getByText('Run the test suite')).toBeTruthy();
|
|
148
|
+
(0, vitest_1.expect)(react_1.screen.getByText('❌ Failed')).toBeTruthy();
|
|
149
|
+
});
|
|
150
|
+
(0, vitest_1.it)('switches runs via the selector chips and resets the caret', () => {
|
|
151
|
+
const second = makeRun({ id: 'run-2', goal: 'Fix flaky test', phases: [
|
|
152
|
+
{ id: 'q1', agentType: 'reviewer', status: 'completed', description: 'Review the diff', durationMs: 5_000 },
|
|
153
|
+
] });
|
|
154
|
+
const onSelectRun = vitest_1.vi.fn();
|
|
155
|
+
(0, react_1.render)(<PhaseTimeline_1.default runs={[makeRun(), second]} onSelectRun={onSelectRun}/>);
|
|
156
|
+
react_1.fireEvent.click(react_1.screen.getByText('Fix flaky test'));
|
|
157
|
+
(0, vitest_1.expect)(onSelectRun).toHaveBeenCalledWith('run-2');
|
|
158
|
+
(0, vitest_1.expect)(react_1.screen.getByText('Review the diff')).toBeTruthy();
|
|
159
|
+
});
|
|
160
|
+
(0, vitest_1.it)('toggles play/pause on the scrub button', () => {
|
|
161
|
+
(0, react_1.render)(<PhaseTimeline_1.default runs={[makeRun()]}/>);
|
|
162
|
+
const playBtn = react_1.screen.getByRole('button', { name: 'Play scrub' });
|
|
163
|
+
react_1.fireEvent.click(playBtn);
|
|
164
|
+
(0, vitest_1.expect)(react_1.screen.getByRole('button', { name: 'Pause scrub' })).toBeTruthy();
|
|
165
|
+
react_1.fireEvent.click(react_1.screen.getByRole('button', { name: 'Pause scrub' }));
|
|
166
|
+
(0, vitest_1.expect)(react_1.screen.getByRole('button', { name: 'Play scrub' })).toBeTruthy();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
// ─── Run derivation helpers ─────────────────────────────────────────────────
|
|
170
|
+
(0, vitest_1.describe)('dagToPipelineRun', () => {
|
|
171
|
+
(0, vitest_1.it)('returns null for empty/missing DAGs', () => {
|
|
172
|
+
(0, vitest_1.expect)((0, PhaseTimeline_1.dagToPipelineRun)(null)).toBeNull();
|
|
173
|
+
(0, vitest_1.expect)((0, PhaseTimeline_1.dagToPipelineRun)(undefined)).toBeNull();
|
|
174
|
+
(0, vitest_1.expect)((0, PhaseTimeline_1.dagToPipelineRun)({ nodes: [] })).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
(0, vitest_1.it)('derives a run with proportional phases from live DAG nodes', () => {
|
|
177
|
+
const dag = {
|
|
178
|
+
pipeline: 'Refactor router',
|
|
179
|
+
nodes: [
|
|
180
|
+
{ id: 'a', agentType: 'planner', status: 'completed', description: 'Plan', startedAt: 100, completedAt: 400 },
|
|
181
|
+
{ id: 'b', agentType: 'writer', status: 'completed', description: 'Write', startedAt: 400, completedAt: 700 },
|
|
182
|
+
],
|
|
183
|
+
edges: [{ from: 'a', to: 'b' }],
|
|
184
|
+
timestamp: 700,
|
|
185
|
+
active: false,
|
|
186
|
+
};
|
|
187
|
+
const run = (0, PhaseTimeline_1.dagToPipelineRun)(dag);
|
|
188
|
+
(0, vitest_1.expect)(run.id).toMatch(/^live-/);
|
|
189
|
+
(0, vitest_1.expect)(run.goal).toBe('Refactor router');
|
|
190
|
+
(0, vitest_1.expect)(run.phases).toHaveLength(2);
|
|
191
|
+
(0, vitest_1.expect)(run.phases[0].durationMs).toBe(300);
|
|
192
|
+
(0, vitest_1.expect)(run.totalDurationMs).toBe(600);
|
|
193
|
+
(0, vitest_1.expect)(run.success).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
(0, vitest_1.it)('marks a run failed when any node failed', () => {
|
|
196
|
+
const dag = {
|
|
197
|
+
pipeline: 'X',
|
|
198
|
+
nodes: [
|
|
199
|
+
{ id: 'a', agentType: 'planner', status: 'completed', description: 'Plan', startedAt: 100, completedAt: 200 },
|
|
200
|
+
{ id: 'b', agentType: 'tester', status: 'failed', description: 'Test', startedAt: 200, completedAt: 300 },
|
|
201
|
+
],
|
|
202
|
+
edges: [],
|
|
203
|
+
timestamp: 300,
|
|
204
|
+
active: false,
|
|
205
|
+
};
|
|
206
|
+
(0, vitest_1.expect)((0, PhaseTimeline_1.dagToPipelineRun)(dag).success).toBe(false);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
(0, vitest_1.describe)('collectPipelineRuns', () => {
|
|
210
|
+
(0, vitest_1.it)('places the live run first and de-duplicates by id', () => {
|
|
211
|
+
// Live-run id is derived from the DAG start time (live-<startedAt>).
|
|
212
|
+
const dag = {
|
|
213
|
+
pipeline: 'Live run',
|
|
214
|
+
nodes: [
|
|
215
|
+
{ id: 'a', agentType: 'planner', status: 'completed', description: 'Plan', startedAt: 100, completedAt: 400 },
|
|
216
|
+
],
|
|
217
|
+
edges: [],
|
|
218
|
+
timestamp: 400,
|
|
219
|
+
active: false,
|
|
220
|
+
};
|
|
221
|
+
const storedRun = makeRun({ id: 'run-9' });
|
|
222
|
+
const runs = (0, PhaseTimeline_1.collectPipelineRuns)(dag, { total: 2, runs: [storedRun, makeRun({ id: 'live-100', goal: 'duplicate' })] });
|
|
223
|
+
(0, vitest_1.expect)(runs.map((r) => r.id)).toEqual(['live-100', 'run-9']);
|
|
224
|
+
});
|
|
225
|
+
(0, vitest_1.it)('falls back to stored runs only when there is no live DAG', () => {
|
|
226
|
+
const storedRun = makeRun();
|
|
227
|
+
const runs = (0, PhaseTimeline_1.collectPipelineRuns)(null, { total: 1, runs: [storedRun] });
|
|
228
|
+
(0, vitest_1.expect)(runs).toHaveLength(1);
|
|
229
|
+
(0, vitest_1.expect)(runs[0].id).toBe('run-1');
|
|
230
|
+
});
|
|
231
|
+
(0, vitest_1.it)('returns an empty list when both inputs are empty', () => {
|
|
232
|
+
(0, vitest_1.expect)((0, PhaseTimeline_1.collectPipelineRuns)(null, undefined)).toEqual([]);
|
|
233
|
+
});
|
|
234
|
+
});
|