mcpgraph-ux 0.1.2 → 0.1.4
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/README.md +6 -0
- package/app/api/execution/breakpoints/route.ts +106 -0
- package/app/api/execution/context/route.ts +69 -0
- package/app/api/execution/controller/route.ts +121 -0
- package/app/api/execution/history/route.ts +40 -0
- package/app/api/execution/history-with-indices/route.ts +57 -0
- package/app/api/execution/stream/route.ts +34 -0
- package/app/api/graph/route.ts +99 -33
- package/app/api/tools/[toolName]/route.ts +338 -21
- package/app/api/tools/route.ts +1 -16
- package/app/page.module.css +64 -18
- package/app/page.tsx +60 -26
- package/components/DebugControls.module.css +124 -0
- package/components/DebugControls.tsx +209 -0
- package/components/ExecutionHistory.module.css +371 -0
- package/components/ExecutionHistory.tsx +272 -0
- package/components/GraphVisualization.module.css +11 -0
- package/components/GraphVisualization.tsx +353 -70
- package/components/InputForm.module.css +146 -0
- package/components/InputForm.tsx +282 -0
- package/components/ServerDetails.module.css +118 -0
- package/components/ServerDetails.tsx +116 -0
- package/components/TelemetryDashboard.module.css +177 -0
- package/components/TelemetryDashboard.tsx +154 -0
- package/components/ToolList.module.css +2 -2
- package/components/ToolTester.module.css +54 -110
- package/components/ToolTester.tsx +746 -235
- package/package.json +8 -9
|
@@ -1,282 +1,793 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useState, useEffect } from 'react';
|
|
3
|
+
import { useState, useEffect, useRef } from 'react';
|
|
4
4
|
import styles from './ToolTester.module.css';
|
|
5
|
+
import { SSEExecutionStream, generateExecutionId, type ExecutionEvent } from '../lib/executionStream';
|
|
6
|
+
import type { NodeExecutionStatus } from './GraphVisualization';
|
|
7
|
+
import ExecutionHistory, { type NodeExecutionRecord } from './ExecutionHistory';
|
|
8
|
+
import DebugControls, { type ExecutionStatus } from './DebugControls';
|
|
9
|
+
import GraphVisualization from './GraphVisualization';
|
|
5
10
|
|
|
6
|
-
|
|
7
|
-
|
|
11
|
+
// Type definitions for SSE event data
|
|
12
|
+
interface NodeCompleteEventData {
|
|
13
|
+
nodeId: string;
|
|
14
|
+
nodeType: string;
|
|
15
|
+
executionIndex: number;
|
|
16
|
+
input: unknown;
|
|
17
|
+
output: unknown;
|
|
18
|
+
duration: number;
|
|
19
|
+
timestamp: number;
|
|
8
20
|
}
|
|
9
21
|
|
|
10
|
-
interface
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
interface NodeErrorEventData {
|
|
23
|
+
nodeId: string;
|
|
24
|
+
nodeType: string;
|
|
25
|
+
executionIndex: number;
|
|
26
|
+
input: unknown; // mcpGraph 0.1.19+ provides actual context
|
|
27
|
+
error: {
|
|
28
|
+
message: string;
|
|
29
|
+
stack?: string;
|
|
30
|
+
code?: number; // MCP error code (e.g., -32000, -32603)
|
|
31
|
+
data?: unknown; // MCP error data/details
|
|
32
|
+
stderr?: string[]; // For ToolCallMcpError - server stderr output
|
|
33
|
+
result?: unknown; // For ToolCallError - tool call result with error
|
|
34
|
+
errorType: 'mcp' | 'tool' | 'unknown'; // Type of error
|
|
21
35
|
};
|
|
36
|
+
timestamp: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface PauseEventData {
|
|
40
|
+
nodeId: string;
|
|
41
|
+
nodeType: string;
|
|
42
|
+
executionIndex: number;
|
|
43
|
+
context: Record<string, unknown>;
|
|
44
|
+
timestamp: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface NodeStartEventData {
|
|
48
|
+
nodeId: string;
|
|
49
|
+
nodeType: string;
|
|
50
|
+
executionIndex: number;
|
|
51
|
+
context: Record<string, unknown>;
|
|
52
|
+
timestamp: number;
|
|
22
53
|
}
|
|
23
54
|
|
|
24
|
-
export
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
55
|
+
export interface ExecutionTelemetry {
|
|
56
|
+
totalDuration: number;
|
|
57
|
+
nodeDurations: Record<string, number>;
|
|
58
|
+
nodeCounts: Record<string, number>;
|
|
59
|
+
errorCount: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Re-export ExecutionStatus for consistency
|
|
63
|
+
export type { ExecutionStatus };
|
|
64
|
+
|
|
65
|
+
interface ToolTesterProps {
|
|
66
|
+
toolName: string;
|
|
67
|
+
graphData: { nodes: Array<{ id: string; type: string; data: Record<string, unknown>; position: { x: number; y: number } }>; edges: Array<{ id: string; source: string; target: string; label?: string }> } | null;
|
|
68
|
+
inputFormRef: React.RefObject<{ submit: (startPaused: boolean) => void }>;
|
|
69
|
+
onFormSubmit: (handler: (formData: Record<string, any>, startPaused: boolean) => void) => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
export default function ToolTester({
|
|
74
|
+
toolName,
|
|
75
|
+
graphData,
|
|
76
|
+
inputFormRef,
|
|
77
|
+
onFormSubmit,
|
|
78
|
+
}: ToolTesterProps) {
|
|
79
|
+
// Expose form submit handler to parent (so InputForm can call it)
|
|
80
|
+
// This needs to be after handleSubmit is defined, so we'll do it in a useEffect
|
|
81
|
+
const formSubmitHandlerRef = useRef<((formData: Record<string, any>, startPaused: boolean) => void) | null>(null);
|
|
28
82
|
const [loading, setLoading] = useState(false);
|
|
29
|
-
const [
|
|
83
|
+
const [executionHistory, setExecutionHistory] = useState<NodeExecutionRecord[]>([]);
|
|
84
|
+
const [telemetry, setTelemetry] = useState<ExecutionTelemetry | null>(null);
|
|
85
|
+
const [executionStatus, setExecutionStatus] = useState<ExecutionStatus>('not_started');
|
|
86
|
+
const [currentNodeId, setCurrentNodeId] = useState<string | null>(null);
|
|
87
|
+
const [currentExecutionId, setCurrentExecutionId] = useState<string | null>(null);
|
|
88
|
+
const [executionState, setExecutionState] = useState<Map<string, NodeExecutionStatus>>(new Map());
|
|
89
|
+
const [highlightedNode, setHighlightedNode] = useState<string | null>(null);
|
|
90
|
+
const [breakpoints, setBreakpoints] = useState<Set<string>>(new Set());
|
|
91
|
+
// Use a ref to always get the latest breakpoints when handleSubmit executes
|
|
92
|
+
const breakpointsRef = useRef<Set<string>>(breakpoints);
|
|
93
|
+
const [executionResult, setExecutionResult] = useState<unknown>(null);
|
|
30
94
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
} else if (prop.type === 'number') {
|
|
48
|
-
defaults[key] = 0;
|
|
49
|
-
} else if (prop.type === 'boolean') {
|
|
50
|
-
defaults[key] = false;
|
|
51
|
-
} else if (prop.type === 'array') {
|
|
52
|
-
defaults[key] = [];
|
|
53
|
-
} else if (prop.type === 'object') {
|
|
54
|
-
defaults[key] = {};
|
|
95
|
+
|
|
96
|
+
// Fetch execution history from controller
|
|
97
|
+
const fetchExecutionHistory = async (execId: string) => {
|
|
98
|
+
try {
|
|
99
|
+
// Use history-with-indices to get executionIndex for each record
|
|
100
|
+
const response = await fetch(`/api/execution/history-with-indices?executionId=${encodeURIComponent(execId)}`);
|
|
101
|
+
if (response.ok) {
|
|
102
|
+
const data = await response.json();
|
|
103
|
+
if (data.history && Array.isArray(data.history)) {
|
|
104
|
+
// Update history and fetch input for any records missing it
|
|
105
|
+
setExecutionHistory(data.history);
|
|
106
|
+
|
|
107
|
+
// Fetch input for any records that don't have it yet
|
|
108
|
+
data.history.forEach((record: NodeExecutionRecord & { executionIndex?: number }) => {
|
|
109
|
+
if (record.executionIndex !== undefined && !record.input) {
|
|
110
|
+
fetchNodeInput(execId, record.nodeId, record.executionIndex);
|
|
55
111
|
}
|
|
56
112
|
});
|
|
57
113
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
.
|
|
61
|
-
|
|
62
|
-
});
|
|
63
|
-
}, [toolName]);
|
|
64
|
-
|
|
65
|
-
const handleInputChange = (key: string, value: any) => {
|
|
66
|
-
setFormData(prev => ({
|
|
67
|
-
...prev,
|
|
68
|
-
[key]: value,
|
|
69
|
-
}));
|
|
114
|
+
}
|
|
115
|
+
} catch (err) {
|
|
116
|
+
console.error('Error fetching execution history:', err);
|
|
117
|
+
}
|
|
70
118
|
};
|
|
119
|
+
|
|
120
|
+
// Fetch input context for a specific node using executionIndex
|
|
121
|
+
const fetchNodeInput = async (execId: string, nodeId: string, executionIndex: number) => {
|
|
122
|
+
try {
|
|
123
|
+
console.log(`[ToolTester] Fetching input for nodeId=${nodeId}, executionIndex=${executionIndex}`);
|
|
124
|
+
const response = await fetch(`/api/execution/context?executionId=${encodeURIComponent(execId)}&nodeId=${encodeURIComponent(nodeId)}&sequenceId=${executionIndex}`);
|
|
125
|
+
if (response.ok) {
|
|
126
|
+
const data = await response.json();
|
|
127
|
+
console.log(`[ToolTester] Got context response for ${nodeId}:`, data);
|
|
128
|
+
if (data.context) {
|
|
129
|
+
// Update the history record with the proper input
|
|
130
|
+
// Match by executionIndex if available, otherwise by nodeId
|
|
131
|
+
setExecutionHistory(prev => {
|
|
132
|
+
const newHistory = prev.map(record => {
|
|
133
|
+
// Match by executionIndex if available (from final history)
|
|
134
|
+
const recordWithIndex: NodeExecutionRecord & { executionIndex?: number } = record;
|
|
135
|
+
if (recordWithIndex.executionIndex === executionIndex) {
|
|
136
|
+
console.log(`[ToolTester] Updating input for record with executionIndex=${executionIndex}`);
|
|
137
|
+
return { ...record, input: data.context };
|
|
138
|
+
}
|
|
139
|
+
// Fallback: match by nodeId if no executionIndex (progressive history)
|
|
140
|
+
if (!recordWithIndex.executionIndex && record.nodeId === nodeId && !record.input) {
|
|
141
|
+
return { ...record, input: data.context };
|
|
142
|
+
}
|
|
143
|
+
return record;
|
|
144
|
+
});
|
|
145
|
+
return newHistory;
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
console.warn(`[ToolTester] No context returned for ${nodeId} at executionIndex=${executionIndex}`);
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
console.error(`[ToolTester] Failed to fetch context for ${nodeId}: ${response.status} ${response.statusText}`);
|
|
152
|
+
}
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.error('Error fetching node input context:', err);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Fetch input context for a node after it completes
|
|
159
|
+
// This gets the executionIndex by fetching the execution history from the API
|
|
160
|
+
const fetchNodeInputAfterComplete = async (execId: string, nodeId: string) => {
|
|
161
|
+
try {
|
|
162
|
+
// Fetch the execution history which should have executionIndex for each record
|
|
163
|
+
// We'll find the most recent record for this nodeId and use its executionIndex
|
|
164
|
+
const response = await fetch(`/api/execution/history-with-indices?executionId=${encodeURIComponent(execId)}`);
|
|
165
|
+
if (response.ok) {
|
|
166
|
+
const data = await response.json();
|
|
167
|
+
if (data.history && Array.isArray(data.history)) {
|
|
168
|
+
// Find the most recent record for this nodeId
|
|
169
|
+
// The history is in execution order, so the last matching record is the most recent
|
|
170
|
+
const records: Array<{ nodeId: string; executionIndex: number }> = Array.isArray(data.history) ? data.history : [];
|
|
171
|
+
let matchingRecord: { nodeId: string; executionIndex: number } | undefined;
|
|
172
|
+
|
|
173
|
+
// Find the last (most recent) record for this nodeId
|
|
174
|
+
for (let i = records.length - 1; i >= 0; i--) {
|
|
175
|
+
if (records[i].nodeId === nodeId) {
|
|
176
|
+
matchingRecord = records[i];
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (matchingRecord) {
|
|
182
|
+
await fetchNodeInput(execId, nodeId, matchingRecord.executionIndex);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.error('Error fetching node input after complete:', err);
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
const executionStreamRef = useRef<SSEExecutionStream | null>(null);
|
|
191
|
+
const executionStateRef = useRef<Map<string, NodeExecutionStatus>>(new Map());
|
|
71
192
|
|
|
72
|
-
const handleSubmit = async (
|
|
73
|
-
|
|
193
|
+
const handleSubmit = async (formData: Record<string, any>, startPaused: boolean = false) => {
|
|
194
|
+
// Read current breakpoints from ref (always up-to-date)
|
|
195
|
+
const currentBreakpoints = breakpointsRef.current;
|
|
196
|
+
|
|
74
197
|
setLoading(true);
|
|
75
|
-
|
|
76
|
-
|
|
198
|
+
setExecutionResult(null);
|
|
199
|
+
setExecutionHistory([]);
|
|
200
|
+
setTelemetry(null);
|
|
201
|
+
setExecutionStatus('not_started');
|
|
202
|
+
setCurrentNodeId(null);
|
|
203
|
+
|
|
204
|
+
// Reset execution state
|
|
205
|
+
executionStateRef.current.clear();
|
|
206
|
+
setExecutionState(new Map());
|
|
207
|
+
|
|
208
|
+
// Generate execution ID and set up SSE stream
|
|
209
|
+
const executionId = generateExecutionId();
|
|
210
|
+
setCurrentExecutionId(executionId);
|
|
211
|
+
// Don't set status here - wait for first event (pause or nodeStart) to tell us actual state
|
|
212
|
+
const stream = new SSEExecutionStream(executionId);
|
|
213
|
+
executionStreamRef.current = stream;
|
|
214
|
+
|
|
215
|
+
// Track if SSE connection is ready (event-driven)
|
|
216
|
+
let sseReady = false;
|
|
217
|
+
let sseReadyResolve: (() => void) | null = null;
|
|
218
|
+
|
|
219
|
+
// Set up event handler
|
|
220
|
+
stream.connect((event: ExecutionEvent) => {
|
|
221
|
+
console.log(`[ToolTester] Received SSE event: ${event.type}`, event);
|
|
222
|
+
const state = executionStateRef.current;
|
|
223
|
+
|
|
224
|
+
switch (event.type) {
|
|
225
|
+
case 'connected':
|
|
226
|
+
console.log(`[ToolTester] SSE connected for execution: ${executionId}`);
|
|
227
|
+
sseReady = true;
|
|
228
|
+
if (sseReadyResolve) {
|
|
229
|
+
sseReadyResolve();
|
|
230
|
+
sseReadyResolve = null;
|
|
231
|
+
}
|
|
232
|
+
break;
|
|
233
|
+
case 'nodeStart': {
|
|
234
|
+
const nodeStartData = event.data as NodeStartEventData;
|
|
235
|
+
if (!nodeStartData || typeof nodeStartData !== 'object' || !('nodeId' in nodeStartData)) {
|
|
236
|
+
console.warn('[ToolTester] Invalid nodeStart event data:', event.data);
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
const existing = state.get(nodeStartData.nodeId);
|
|
240
|
+
state.set(nodeStartData.nodeId, {
|
|
241
|
+
nodeId: nodeStartData.nodeId,
|
|
242
|
+
state: 'running',
|
|
243
|
+
startTime: existing?.startTime || nodeStartData.timestamp,
|
|
244
|
+
endTime: undefined,
|
|
245
|
+
duration: undefined,
|
|
246
|
+
});
|
|
247
|
+
if (nodeStartData.nodeId) {
|
|
248
|
+
setCurrentNodeId(nodeStartData.nodeId);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Update or create history record for this node with input from context
|
|
252
|
+
// If we already created a pending record from pause event, update it with nodeType
|
|
253
|
+
// Otherwise, create a new running record
|
|
254
|
+
const executionIndex = nodeStartData.executionIndex;
|
|
255
|
+
setExecutionHistory(prev => {
|
|
256
|
+
const existingIndex = prev.findIndex(
|
|
257
|
+
r => r.nodeId === nodeStartData.nodeId && r.executionIndex === executionIndex
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
if (existingIndex >= 0) {
|
|
261
|
+
// Update existing record (created from pause event)
|
|
262
|
+
const updated = [...prev];
|
|
263
|
+
updated[existingIndex] = {
|
|
264
|
+
...updated[existingIndex],
|
|
265
|
+
nodeType: nodeStartData.nodeType,
|
|
266
|
+
startTime: nodeStartData.timestamp,
|
|
267
|
+
input: nodeStartData.context, // Update with context from nodeStart (more accurate)
|
|
268
|
+
};
|
|
269
|
+
return updated;
|
|
270
|
+
} else {
|
|
271
|
+
// Create new running record
|
|
272
|
+
const newHistory = [...prev, {
|
|
273
|
+
nodeId: nodeStartData.nodeId,
|
|
274
|
+
nodeType: nodeStartData.nodeType,
|
|
275
|
+
startTime: nodeStartData.timestamp,
|
|
276
|
+
endTime: undefined, // Not completed yet
|
|
277
|
+
duration: undefined, // Not completed yet
|
|
278
|
+
input: nodeStartData.context, // Use context as input
|
|
279
|
+
output: undefined, // Not completed yet
|
|
280
|
+
executionIndex,
|
|
281
|
+
}];
|
|
282
|
+
return newHistory;
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
case 'nodeComplete': {
|
|
288
|
+
const eventData = event.data as NodeCompleteEventData;
|
|
289
|
+
if (!eventData || typeof eventData !== 'object' || !('nodeId' in eventData)) {
|
|
290
|
+
console.warn('[ToolTester] Invalid nodeComplete event data:', event.data);
|
|
291
|
+
break;
|
|
292
|
+
}
|
|
293
|
+
const existing = state.get(eventData.nodeId);
|
|
294
|
+
state.set(eventData.nodeId, {
|
|
295
|
+
nodeId: eventData.nodeId,
|
|
296
|
+
state: 'completed',
|
|
297
|
+
startTime: existing?.startTime || eventData.timestamp,
|
|
298
|
+
endTime: eventData.timestamp,
|
|
299
|
+
duration: eventData.duration,
|
|
300
|
+
});
|
|
301
|
+
// Build history record immediately for progressive display
|
|
302
|
+
// Use input from the event if available, otherwise fetch it
|
|
303
|
+
const startTime = existing?.startTime || eventData.timestamp;
|
|
304
|
+
const executionIndex = eventData.executionIndex;
|
|
305
|
+
const inputFromEvent = eventData.input;
|
|
306
|
+
|
|
307
|
+
setExecutionHistory(prev => {
|
|
308
|
+
// Check if we already have a record for this node (created from pause/nodeStart)
|
|
309
|
+
const existingIndex = prev.findIndex(
|
|
310
|
+
r => r.nodeId === eventData.nodeId && r.executionIndex === executionIndex
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
if (existingIndex >= 0) {
|
|
314
|
+
// Update existing record (created from pause/nodeStart)
|
|
315
|
+
const updated = [...prev];
|
|
316
|
+
updated[existingIndex] = {
|
|
317
|
+
...updated[existingIndex],
|
|
318
|
+
nodeType: eventData.nodeType,
|
|
319
|
+
startTime,
|
|
320
|
+
endTime: eventData.timestamp,
|
|
321
|
+
duration: eventData.duration,
|
|
322
|
+
input: inputFromEvent, // Use input from event
|
|
323
|
+
output: eventData.output,
|
|
324
|
+
executionIndex,
|
|
325
|
+
};
|
|
326
|
+
return updated;
|
|
327
|
+
} else {
|
|
328
|
+
// Create new record
|
|
329
|
+
const newHistory = [...prev, {
|
|
330
|
+
nodeId: eventData.nodeId,
|
|
331
|
+
nodeType: eventData.nodeType,
|
|
332
|
+
startTime,
|
|
333
|
+
endTime: eventData.timestamp,
|
|
334
|
+
duration: eventData.duration,
|
|
335
|
+
input: inputFromEvent, // Use input from event
|
|
336
|
+
output: eventData.output,
|
|
337
|
+
executionIndex,
|
|
338
|
+
}];
|
|
339
|
+
return newHistory;
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
// If input wasn't in the event, fetch it using executionIndex
|
|
344
|
+
if (currentExecutionId && inputFromEvent === undefined) {
|
|
345
|
+
fetchNodeInput(currentExecutionId, eventData.nodeId, executionIndex);
|
|
346
|
+
}
|
|
347
|
+
break;
|
|
348
|
+
}
|
|
349
|
+
case 'nodeError': {
|
|
350
|
+
const eventData = event.data as NodeErrorEventData;
|
|
351
|
+
if (!eventData || typeof eventData !== 'object' || !('nodeId' in eventData)) {
|
|
352
|
+
console.warn('[ToolTester] Invalid nodeError event data:', event.data);
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
const existingError = state.get(eventData.nodeId);
|
|
356
|
+
state.set(eventData.nodeId, {
|
|
357
|
+
nodeId: eventData.nodeId,
|
|
358
|
+
state: 'error',
|
|
359
|
+
startTime: existingError?.startTime || eventData.timestamp,
|
|
360
|
+
endTime: eventData.timestamp,
|
|
361
|
+
error: eventData.error?.message || 'Unknown error',
|
|
362
|
+
});
|
|
363
|
+
// Build history record immediately for progressive display
|
|
364
|
+
// Use input from the event (mcpGraph 0.1.19+ provides actual context)
|
|
365
|
+
const errorStartTime = existingError?.startTime || eventData.timestamp;
|
|
366
|
+
const executionIndex = eventData.executionIndex;
|
|
367
|
+
const inputFromEvent = eventData.input; // Context is now always provided
|
|
368
|
+
|
|
369
|
+
setExecutionHistory(prev => {
|
|
370
|
+
// Check if we already have a record for this node (created from pause/nodeStart)
|
|
371
|
+
const existingIndex = prev.findIndex(
|
|
372
|
+
r => r.nodeId === eventData.nodeId && r.executionIndex === executionIndex
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
if (existingIndex >= 0) {
|
|
376
|
+
// Update existing record (created from pause/nodeStart)
|
|
377
|
+
const updated = [...prev];
|
|
378
|
+
updated[existingIndex] = {
|
|
379
|
+
...updated[existingIndex],
|
|
380
|
+
nodeType: eventData.nodeType,
|
|
381
|
+
startTime: errorStartTime,
|
|
382
|
+
endTime: eventData.timestamp,
|
|
383
|
+
duration: eventData.timestamp - errorStartTime,
|
|
384
|
+
input: inputFromEvent, // Use input from event
|
|
385
|
+
output: undefined,
|
|
386
|
+
error: {
|
|
387
|
+
message: eventData.error.message,
|
|
388
|
+
stack: eventData.error.stack,
|
|
389
|
+
...(eventData.error.code !== undefined && { code: eventData.error.code }),
|
|
390
|
+
...(eventData.error.data !== undefined && { data: eventData.error.data }),
|
|
391
|
+
...(eventData.error.stderr !== undefined && { stderr: eventData.error.stderr }),
|
|
392
|
+
...(eventData.error.result !== undefined && { result: eventData.error.result }),
|
|
393
|
+
...(eventData.error.errorType !== undefined && { errorType: eventData.error.errorType }),
|
|
394
|
+
} as Error & {
|
|
395
|
+
code?: number;
|
|
396
|
+
data?: unknown;
|
|
397
|
+
stderr?: string[];
|
|
398
|
+
result?: unknown;
|
|
399
|
+
errorType?: 'mcp' | 'tool' | 'unknown';
|
|
400
|
+
},
|
|
401
|
+
executionIndex,
|
|
402
|
+
};
|
|
403
|
+
return updated;
|
|
404
|
+
} else {
|
|
405
|
+
// Create new record
|
|
406
|
+
const newHistory = [...prev, {
|
|
407
|
+
nodeId: eventData.nodeId,
|
|
408
|
+
nodeType: eventData.nodeType,
|
|
409
|
+
startTime: errorStartTime,
|
|
410
|
+
endTime: eventData.timestamp,
|
|
411
|
+
duration: eventData.timestamp - errorStartTime,
|
|
412
|
+
input: inputFromEvent, // Use input from event
|
|
413
|
+
output: undefined,
|
|
414
|
+
error: {
|
|
415
|
+
message: eventData.error.message,
|
|
416
|
+
stack: eventData.error.stack,
|
|
417
|
+
...(eventData.error.code !== undefined && { code: eventData.error.code }),
|
|
418
|
+
...(eventData.error.data !== undefined && { data: eventData.error.data }),
|
|
419
|
+
...(eventData.error.stderr !== undefined && { stderr: eventData.error.stderr }),
|
|
420
|
+
...(eventData.error.result !== undefined && { result: eventData.error.result }),
|
|
421
|
+
...(eventData.error.errorType !== undefined && { errorType: eventData.error.errorType }),
|
|
422
|
+
} as Error & {
|
|
423
|
+
code?: number;
|
|
424
|
+
data?: unknown;
|
|
425
|
+
stderr?: string[];
|
|
426
|
+
result?: unknown;
|
|
427
|
+
errorType?: 'mcp' | 'tool' | 'unknown';
|
|
428
|
+
},
|
|
429
|
+
executionIndex,
|
|
430
|
+
}];
|
|
431
|
+
return newHistory;
|
|
432
|
+
}
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
// If input wasn't in the event, fetch it using executionIndex
|
|
436
|
+
if (currentExecutionId && inputFromEvent === undefined) {
|
|
437
|
+
fetchNodeInput(currentExecutionId, eventData.nodeId, executionIndex);
|
|
438
|
+
}
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
case 'executionComplete': {
|
|
442
|
+
const completeData = event.data as { result?: unknown; executionHistory?: Array<NodeExecutionRecord & { executionIndex?: number }> };
|
|
443
|
+
if (!completeData || typeof completeData !== 'object') {
|
|
444
|
+
console.warn('[ToolTester] Invalid executionComplete event data:', event.data);
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
console.log(`[ToolTester] Execution complete, result:`, completeData.result);
|
|
448
|
+
setExecutionResult(completeData.result);
|
|
449
|
+
// The execution history from the API should already have input populated
|
|
450
|
+
// since we fetch it before unregistering the controller
|
|
451
|
+
if (Array.isArray(completeData.executionHistory)) {
|
|
452
|
+
setExecutionHistory(completeData.executionHistory);
|
|
453
|
+
}
|
|
454
|
+
if ('telemetry' in completeData && completeData.telemetry) {
|
|
455
|
+
setTelemetry(completeData.telemetry as ExecutionTelemetry);
|
|
456
|
+
}
|
|
457
|
+
setExecutionStatus('finished');
|
|
458
|
+
setCurrentNodeId(null);
|
|
459
|
+
setLoading(false);
|
|
460
|
+
stream.disconnect();
|
|
461
|
+
executionStreamRef.current = null;
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
case 'executionError': {
|
|
465
|
+
if (typeof event.data !== 'object' || !event.data) {
|
|
466
|
+
console.warn('[ToolTester] Invalid executionError event data:', event.data);
|
|
467
|
+
break;
|
|
468
|
+
}
|
|
469
|
+
const errorData = event.data as { error?: string | { message?: string } };
|
|
470
|
+
const fullErrorMessage = typeof errorData.error === 'string'
|
|
471
|
+
? errorData.error
|
|
472
|
+
: (typeof errorData.error === 'object' && errorData.error?.message) || 'Unknown error';
|
|
473
|
+
// Extract just the first line for the summary (before stderr/details)
|
|
474
|
+
const errorSummary = fullErrorMessage.split('\n')[0].split('Server stderr')[0].trim();
|
|
475
|
+
console.log(`[ToolTester] Execution error:`, fullErrorMessage);
|
|
476
|
+
// Set error as the execution result so it shows in history - just the summary
|
|
477
|
+
setExecutionResult({ error: errorSummary });
|
|
478
|
+
setExecutionStatus('error');
|
|
479
|
+
setCurrentNodeId(null);
|
|
480
|
+
setLoading(false);
|
|
481
|
+
stream.disconnect();
|
|
482
|
+
executionStreamRef.current = null;
|
|
483
|
+
setCurrentExecutionId(null);
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
case 'executionStopped':
|
|
487
|
+
console.log(`[ToolTester] Execution stopped by user`);
|
|
488
|
+
setExecutionStatus('stopped');
|
|
489
|
+
setCurrentNodeId(null);
|
|
490
|
+
setLoading(false);
|
|
491
|
+
stream.disconnect();
|
|
492
|
+
executionStreamRef.current = null;
|
|
493
|
+
setCurrentExecutionId(null);
|
|
494
|
+
break;
|
|
495
|
+
case 'pause': {
|
|
496
|
+
const pauseData = event.data as PauseEventData;
|
|
497
|
+
if (!pauseData || typeof pauseData !== 'object' || !('nodeId' in pauseData)) {
|
|
498
|
+
console.warn('[ToolTester] Invalid pause event data:', event.data);
|
|
499
|
+
break;
|
|
500
|
+
}
|
|
501
|
+
console.log(`[ToolTester] Pause event received for node: ${pauseData.nodeId}`);
|
|
502
|
+
setExecutionStatus('paused');
|
|
503
|
+
if (pauseData.nodeId) {
|
|
504
|
+
setCurrentNodeId(pauseData.nodeId);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Create a pending history record for the node we're paused on
|
|
508
|
+
// This allows the user to see the node's input even though it hasn't completed yet
|
|
509
|
+
const executionIndex = pauseData.executionIndex;
|
|
510
|
+
const existingRecord = executionHistory.find(
|
|
511
|
+
r => r.nodeId === pauseData.nodeId && r.executionIndex === executionIndex
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
if (!existingRecord && pauseData.context) {
|
|
515
|
+
// Extract input from context - the context contains all available data at this point
|
|
516
|
+
// For the node about to execute, we need to determine what its input would be
|
|
517
|
+
// The context is the execution context, which includes outputs from previous nodes
|
|
518
|
+
// For now, we'll use the context as the input (it represents what's available to this node)
|
|
519
|
+
setExecutionHistory(prev => {
|
|
520
|
+
const newHistory = [...prev, {
|
|
521
|
+
nodeId: pauseData.nodeId,
|
|
522
|
+
nodeType: pauseData.nodeType, // Use nodeType from pause event
|
|
523
|
+
startTime: pauseData.timestamp,
|
|
524
|
+
endTime: undefined, // Not completed yet
|
|
525
|
+
duration: undefined, // Not completed yet
|
|
526
|
+
input: pauseData.context, // Use context as input (what's available to this node)
|
|
527
|
+
output: undefined, // Not completed yet
|
|
528
|
+
executionIndex,
|
|
529
|
+
}];
|
|
530
|
+
return newHistory;
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Fetch execution history from controller on pause to get any completed nodes
|
|
535
|
+
if (currentExecutionId) {
|
|
536
|
+
fetchExecutionHistory(currentExecutionId);
|
|
537
|
+
}
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
case 'resume':
|
|
541
|
+
// Don't set status here - stateUpdate is the authoritative source
|
|
542
|
+
// The resume event is just informational, stateUpdate will follow with the actual status
|
|
543
|
+
break;
|
|
544
|
+
case 'stateUpdate': {
|
|
545
|
+
const stateData = event.data as { status?: ExecutionStatus; currentNodeId?: string | null };
|
|
546
|
+
console.log(`[ToolTester] stateUpdate event:`, stateData);
|
|
547
|
+
if (stateData.status) {
|
|
548
|
+
console.log(`[ToolTester] Setting execution status to: ${stateData.status}`);
|
|
549
|
+
setExecutionStatus(stateData.status);
|
|
550
|
+
}
|
|
551
|
+
if (stateData.currentNodeId !== undefined) {
|
|
552
|
+
setCurrentNodeId(stateData.currentNodeId);
|
|
553
|
+
} else if (stateData.status === 'running' || stateData.status === 'finished' || stateData.status === 'error' || stateData.status === 'stopped') {
|
|
554
|
+
// Clear currentNodeId when execution is no longer paused
|
|
555
|
+
setCurrentNodeId(null);
|
|
556
|
+
}
|
|
557
|
+
break;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Update execution state
|
|
562
|
+
setExecutionState(new Map(state));
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
// Wait for SSE connection to be ready (event-driven)
|
|
566
|
+
const waitForSSE = new Promise<void>((resolve) => {
|
|
567
|
+
if (sseReady) {
|
|
568
|
+
resolve();
|
|
569
|
+
} else {
|
|
570
|
+
sseReadyResolve = resolve;
|
|
571
|
+
// Safety timeout in case connected event never arrives
|
|
572
|
+
setTimeout(() => {
|
|
573
|
+
if (!sseReady && sseReadyResolve) {
|
|
574
|
+
console.warn(`[ToolTester] SSE connection not ready after 2s, proceeding anyway`);
|
|
575
|
+
sseReadyResolve = null;
|
|
576
|
+
resolve();
|
|
577
|
+
}
|
|
578
|
+
}, 2000);
|
|
579
|
+
}
|
|
580
|
+
});
|
|
77
581
|
|
|
78
582
|
try {
|
|
583
|
+
await waitForSSE;
|
|
584
|
+
console.log(`[ToolTester] Starting execution for tool: ${toolName}, executionId: ${executionId}`);
|
|
585
|
+
|
|
586
|
+
const breakpointsArray = Array.from(currentBreakpoints);
|
|
587
|
+
|
|
79
588
|
const response = await fetch(`/api/tools/${toolName}`, {
|
|
80
589
|
method: 'POST',
|
|
81
590
|
headers: {
|
|
82
591
|
'Content-Type': 'application/json',
|
|
83
592
|
},
|
|
84
|
-
body: JSON.stringify({
|
|
593
|
+
body: JSON.stringify({
|
|
594
|
+
args: formData,
|
|
595
|
+
executionId,
|
|
596
|
+
options: {
|
|
597
|
+
enableTelemetry: true,
|
|
598
|
+
breakpoints: breakpointsArray,
|
|
599
|
+
startPaused: startPaused, // mcpGraph 0.1.12+ supports starting paused
|
|
600
|
+
},
|
|
601
|
+
}),
|
|
85
602
|
});
|
|
86
603
|
|
|
87
604
|
const data = await response.json();
|
|
88
605
|
|
|
89
606
|
if (data.error) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
607
|
+
// Show error in execution history, not as a banner - just the summary
|
|
608
|
+
const fullError = typeof data.error === 'string'
|
|
609
|
+
? data.error
|
|
610
|
+
: (typeof data.error === 'object' && data.error !== null && 'message' in data.error)
|
|
611
|
+
? String((data.error as { message: unknown }).message)
|
|
612
|
+
: 'Unknown error';
|
|
613
|
+
// Extract just the first line for the summary (before stderr/details)
|
|
614
|
+
const errorSummary = fullError.split('\n')[0].split('Server stderr')[0].trim();
|
|
615
|
+
setExecutionResult({ error: errorSummary });
|
|
616
|
+
setExecutionStatus('error');
|
|
617
|
+
setLoading(false);
|
|
618
|
+
stream.disconnect();
|
|
619
|
+
executionStreamRef.current = null;
|
|
93
620
|
}
|
|
621
|
+
// Result will be set via SSE executionComplete event
|
|
94
622
|
} catch (err) {
|
|
95
|
-
|
|
96
|
-
|
|
623
|
+
console.error(`[ToolTester] Error executing tool:`, err);
|
|
624
|
+
// Show error in execution history, not as a banner
|
|
625
|
+
setExecutionResult({ error: err instanceof Error ? err.message : 'Unknown error' });
|
|
626
|
+
setExecutionStatus('error');
|
|
97
627
|
setLoading(false);
|
|
628
|
+
stream.disconnect();
|
|
629
|
+
executionStreamRef.current = null;
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
|
|
633
|
+
// Expose handlers for DebugControls - trigger form submission
|
|
634
|
+
const handleRun = () => {
|
|
635
|
+
if (inputFormRef.current) {
|
|
636
|
+
inputFormRef.current.submit(false);
|
|
637
|
+
} else {
|
|
638
|
+
console.warn('[ToolTester] inputFormRef.current is null, cannot submit form');
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
const handleStep = () => {
|
|
642
|
+
if (inputFormRef.current) {
|
|
643
|
+
inputFormRef.current.submit(true);
|
|
644
|
+
} else {
|
|
645
|
+
console.warn('[ToolTester] inputFormRef.current is null, cannot submit form');
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const handleClear = () => {
|
|
650
|
+
// Reset all execution-related state
|
|
651
|
+
setLoading(false);
|
|
652
|
+
setExecutionResult(null);
|
|
653
|
+
setExecutionHistory([]);
|
|
654
|
+
setTelemetry(null);
|
|
655
|
+
setExecutionStatus('not_started');
|
|
656
|
+
setCurrentNodeId(null);
|
|
657
|
+
setCurrentExecutionId(null);
|
|
658
|
+
setExecutionState(new Map());
|
|
659
|
+
setHighlightedNode(null);
|
|
660
|
+
const emptyBreakpoints = new Set<string>();
|
|
661
|
+
setBreakpoints(emptyBreakpoints);
|
|
662
|
+
// Update ref immediately
|
|
663
|
+
breakpointsRef.current = emptyBreakpoints;
|
|
664
|
+
|
|
665
|
+
// Clear execution state ref
|
|
666
|
+
executionStateRef.current.clear();
|
|
667
|
+
|
|
668
|
+
// Disconnect any active stream
|
|
669
|
+
if (executionStreamRef.current) {
|
|
670
|
+
executionStreamRef.current.disconnect();
|
|
671
|
+
executionStreamRef.current = null;
|
|
98
672
|
}
|
|
99
673
|
};
|
|
674
|
+
|
|
675
|
+
// Keep breakpointsRef in sync with breakpoints state
|
|
676
|
+
useEffect(() => {
|
|
677
|
+
breakpointsRef.current = breakpoints;
|
|
678
|
+
}, [breakpoints]);
|
|
100
679
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
680
|
+
// Reset execution state when toolName changes (switching to a different tool)
|
|
681
|
+
useEffect(() => {
|
|
682
|
+
// Clear all execution-related state when tool changes
|
|
683
|
+
setLoading(false);
|
|
684
|
+
setExecutionResult(null);
|
|
685
|
+
setExecutionHistory([]);
|
|
686
|
+
setTelemetry(null);
|
|
687
|
+
setExecutionStatus('not_started');
|
|
688
|
+
setCurrentNodeId(null);
|
|
689
|
+
setCurrentExecutionId(null);
|
|
690
|
+
setExecutionState(new Map());
|
|
691
|
+
setHighlightedNode(null);
|
|
692
|
+
const emptyBreakpoints = new Set<string>();
|
|
693
|
+
setBreakpoints(emptyBreakpoints);
|
|
694
|
+
breakpointsRef.current = emptyBreakpoints;
|
|
695
|
+
executionStateRef.current.clear();
|
|
696
|
+
|
|
697
|
+
// Disconnect any active stream
|
|
698
|
+
if (executionStreamRef.current) {
|
|
699
|
+
executionStreamRef.current.disconnect();
|
|
700
|
+
executionStreamRef.current = null;
|
|
701
|
+
}
|
|
702
|
+
}, [toolName]);
|
|
104
703
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
case 'boolean':
|
|
145
|
-
return (
|
|
146
|
-
<div key={key} className={styles.field}>
|
|
147
|
-
<label className={styles.checkboxLabel}>
|
|
148
|
-
<input
|
|
149
|
-
type="checkbox"
|
|
150
|
-
checked={value}
|
|
151
|
-
onChange={e => handleInputChange(key, e.target.checked)}
|
|
152
|
-
className={styles.checkbox}
|
|
153
|
-
/>
|
|
154
|
-
{key}
|
|
155
|
-
{isRequired && <span className={styles.required}>*</span>}
|
|
156
|
-
</label>
|
|
157
|
-
{prop.description && (
|
|
158
|
-
<div className={styles.hint}>{prop.description}</div>
|
|
159
|
-
)}
|
|
160
|
-
</div>
|
|
161
|
-
);
|
|
162
|
-
case 'array':
|
|
163
|
-
return (
|
|
164
|
-
<div key={key} className={styles.field}>
|
|
165
|
-
<label className={styles.label}>
|
|
166
|
-
{key}
|
|
167
|
-
{isRequired && <span className={styles.required}>*</span>}
|
|
168
|
-
</label>
|
|
169
|
-
<textarea
|
|
170
|
-
value={Array.isArray(value) ? JSON.stringify(value, null, 2) : '[]'}
|
|
171
|
-
onChange={e => {
|
|
172
|
-
try {
|
|
173
|
-
const parsed = JSON.parse(e.target.value);
|
|
174
|
-
handleInputChange(key, parsed);
|
|
175
|
-
} catch {
|
|
176
|
-
// Invalid JSON, ignore
|
|
177
|
-
}
|
|
178
|
-
}}
|
|
179
|
-
className={styles.textarea}
|
|
180
|
-
placeholder={prop.description || `Enter ${key} as JSON array`}
|
|
181
|
-
rows={3}
|
|
182
|
-
/>
|
|
183
|
-
{prop.description && (
|
|
184
|
-
<div className={styles.hint}>{prop.description}</div>
|
|
185
|
-
)}
|
|
186
|
-
</div>
|
|
187
|
-
);
|
|
188
|
-
case 'object':
|
|
189
|
-
return (
|
|
190
|
-
<div key={key} className={styles.field}>
|
|
191
|
-
<label className={styles.label}>
|
|
192
|
-
{key}
|
|
193
|
-
{isRequired && <span className={styles.required}>*</span>}
|
|
194
|
-
</label>
|
|
195
|
-
<textarea
|
|
196
|
-
value={typeof value === 'object' ? JSON.stringify(value, null, 2) : '{}'}
|
|
197
|
-
onChange={e => {
|
|
198
|
-
try {
|
|
199
|
-
const parsed = JSON.parse(e.target.value);
|
|
200
|
-
handleInputChange(key, parsed);
|
|
201
|
-
} catch {
|
|
202
|
-
// Invalid JSON, ignore
|
|
203
|
-
}
|
|
204
|
-
}}
|
|
205
|
-
className={styles.textarea}
|
|
206
|
-
placeholder={prop.description || `Enter ${key} as JSON object`}
|
|
207
|
-
rows={5}
|
|
208
|
-
/>
|
|
209
|
-
{prop.description && (
|
|
210
|
-
<div className={styles.hint}>{prop.description}</div>
|
|
211
|
-
)}
|
|
212
|
-
</div>
|
|
213
|
-
);
|
|
214
|
-
default:
|
|
215
|
-
return (
|
|
216
|
-
<div key={key} className={styles.field}>
|
|
217
|
-
<label className={styles.label}>
|
|
218
|
-
{key}
|
|
219
|
-
{isRequired && <span className={styles.required}>*</span>}
|
|
220
|
-
</label>
|
|
221
|
-
<textarea
|
|
222
|
-
value={typeof value === 'string' ? value : JSON.stringify(value)}
|
|
223
|
-
onChange={e => {
|
|
224
|
-
try {
|
|
225
|
-
const parsed = JSON.parse(e.target.value);
|
|
226
|
-
handleInputChange(key, parsed);
|
|
227
|
-
} catch {
|
|
228
|
-
handleInputChange(key, e.target.value);
|
|
229
|
-
}
|
|
230
|
-
}}
|
|
231
|
-
className={styles.textarea}
|
|
232
|
-
placeholder={prop.description || `Enter ${key}`}
|
|
233
|
-
rows={3}
|
|
234
|
-
/>
|
|
235
|
-
{prop.description && (
|
|
236
|
-
<div className={styles.hint}>{prop.description}</div>
|
|
237
|
-
)}
|
|
238
|
-
</div>
|
|
239
|
-
);
|
|
704
|
+
// Expose form submit handler to parent (so InputForm can call it)
|
|
705
|
+
// Update when toolName changes to ensure we use the correct tool
|
|
706
|
+
useEffect(() => {
|
|
707
|
+
// Update the ref with the current handleSubmit
|
|
708
|
+
// The handleSubmit function will read breakpoints from breakpointsRef when called
|
|
709
|
+
formSubmitHandlerRef.current = handleSubmit;
|
|
710
|
+
|
|
711
|
+
// Expose the handler to parent
|
|
712
|
+
if (onFormSubmit) {
|
|
713
|
+
onFormSubmit((formData: Record<string, any>, startPaused: boolean) => {
|
|
714
|
+
if (formSubmitHandlerRef.current) {
|
|
715
|
+
formSubmitHandlerRef.current(formData, startPaused);
|
|
716
|
+
} else {
|
|
717
|
+
console.warn('[ToolTester] formSubmitHandlerRef.current is null');
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
} else {
|
|
721
|
+
console.warn('[ToolTester] onFormSubmit is not provided');
|
|
722
|
+
}
|
|
723
|
+
// Update handler when toolName changes so it uses the correct tool
|
|
724
|
+
// handleSubmit is recreated on every render but uses toolName from props, so we need to update the ref
|
|
725
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
726
|
+
}, [toolName, onFormSubmit]);
|
|
727
|
+
|
|
728
|
+
// Cleanup on unmount
|
|
729
|
+
useEffect(() => {
|
|
730
|
+
return () => {
|
|
731
|
+
if (executionStreamRef.current) {
|
|
732
|
+
executionStreamRef.current.disconnect();
|
|
733
|
+
}
|
|
734
|
+
};
|
|
735
|
+
}, []);
|
|
736
|
+
|
|
737
|
+
const handleToggleBreakpoint = (nodeId: string) => {
|
|
738
|
+
const newBreakpoints = new Set(breakpoints);
|
|
739
|
+
if (newBreakpoints.has(nodeId)) {
|
|
740
|
+
newBreakpoints.delete(nodeId);
|
|
741
|
+
} else {
|
|
742
|
+
newBreakpoints.add(nodeId);
|
|
240
743
|
}
|
|
744
|
+
setBreakpoints(newBreakpoints);
|
|
745
|
+
// Update ref immediately so handleSubmit always has the latest value
|
|
746
|
+
breakpointsRef.current = newBreakpoints;
|
|
241
747
|
};
|
|
242
748
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
749
|
+
const handleNodeClick = (nodeId: string) => {
|
|
750
|
+
setHighlightedNode(nodeId);
|
|
751
|
+
setTimeout(() => setHighlightedNode(null), 2000);
|
|
752
|
+
};
|
|
246
753
|
|
|
247
754
|
return (
|
|
248
755
|
<div className={styles.container}>
|
|
249
|
-
<
|
|
250
|
-
<
|
|
251
|
-
{
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
756
|
+
<div className={styles.debugControlsHeader}>
|
|
757
|
+
<DebugControls
|
|
758
|
+
executionId={currentExecutionId}
|
|
759
|
+
status={executionStatus}
|
|
760
|
+
currentNodeId={currentNodeId}
|
|
761
|
+
onRun={handleRun}
|
|
762
|
+
onStepFromStart={handleStep}
|
|
763
|
+
onClear={handleClear}
|
|
764
|
+
/>
|
|
765
|
+
</div>
|
|
766
|
+
<div className={styles.graphHistoryContainer}>
|
|
767
|
+
<div className={styles.graphSection}>
|
|
768
|
+
{graphData && (
|
|
769
|
+
<GraphVisualization
|
|
770
|
+
nodes={graphData.nodes}
|
|
771
|
+
edges={graphData.edges}
|
|
772
|
+
selectedTool={toolName}
|
|
773
|
+
executionState={executionState}
|
|
774
|
+
highlightedNode={highlightedNode}
|
|
775
|
+
currentNodeId={currentNodeId}
|
|
776
|
+
breakpoints={breakpoints}
|
|
777
|
+
onToggleBreakpoint={handleToggleBreakpoint}
|
|
778
|
+
onNodeClick={handleNodeClick}
|
|
779
|
+
/>
|
|
780
|
+
)}
|
|
269
781
|
</div>
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
</pre>
|
|
782
|
+
<div className={styles.historySection}>
|
|
783
|
+
<ExecutionHistory
|
|
784
|
+
history={executionHistory}
|
|
785
|
+
result={executionResult}
|
|
786
|
+
telemetry={telemetry || undefined}
|
|
787
|
+
onNodeClick={handleNodeClick}
|
|
788
|
+
/>
|
|
278
789
|
</div>
|
|
279
|
-
|
|
790
|
+
</div>
|
|
280
791
|
</div>
|
|
281
792
|
);
|
|
282
793
|
}
|