mcpgraph-ux 0.1.1 → 0.1.3

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.
@@ -0,0 +1,209 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import styles from './DebugControls.module.css';
5
+
6
+ export type ExecutionStatus = 'not_started' | 'running' | 'paused' | 'finished' | 'error' | 'stopped';
7
+
8
+ interface DebugControlsProps {
9
+ executionId: string | null;
10
+ status: ExecutionStatus;
11
+ currentNodeId: string | null;
12
+ onStatusChange?: (status: ExecutionStatus) => void;
13
+ onRun?: () => void;
14
+ onStepFromStart?: () => void;
15
+ onClear?: () => void;
16
+ disabled?: boolean;
17
+ }
18
+
19
+ export default function DebugControls({
20
+ executionId,
21
+ status,
22
+ currentNodeId,
23
+ onStatusChange,
24
+ onRun,
25
+ onStepFromStart,
26
+ onClear,
27
+ disabled,
28
+ }: DebugControlsProps) {
29
+ const [isProcessing, setIsProcessing] = useState(false);
30
+
31
+ const handlePause = async () => {
32
+ if (!executionId || status !== 'running') return;
33
+
34
+ setIsProcessing(true);
35
+ try {
36
+ const response = await fetch('/api/execution/controller', {
37
+ method: 'POST',
38
+ headers: { 'Content-Type': 'application/json' },
39
+ body: JSON.stringify({ executionId, action: 'pause' }),
40
+ });
41
+
42
+ const data = await response.json();
43
+ if (data.error) {
44
+ console.error('Error pausing:', data.error);
45
+ }
46
+ // Don't set status here - onPause hook will send stateUpdate with actual state
47
+ } catch (error) {
48
+ console.error('Error pausing execution:', error);
49
+ } finally {
50
+ setIsProcessing(false);
51
+ }
52
+ };
53
+
54
+ const handleResume = async () => {
55
+ if (!executionId || status !== 'paused') return;
56
+
57
+ setIsProcessing(true);
58
+ try {
59
+ const response = await fetch('/api/execution/controller', {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify({ executionId, action: 'resume' }),
63
+ });
64
+
65
+ const data = await response.json();
66
+ if (data.error) {
67
+ console.error('Error resuming:', data.error);
68
+ }
69
+ // Don't set status here - hooks will tell us the actual state
70
+ } catch (error) {
71
+ console.error('Error resuming execution:', error);
72
+ } finally {
73
+ setIsProcessing(false);
74
+ }
75
+ };
76
+
77
+ const handleStep = async () => {
78
+ if (!executionId || status !== 'paused') return;
79
+
80
+ setIsProcessing(true);
81
+ try {
82
+ const response = await fetch('/api/execution/controller', {
83
+ method: 'POST',
84
+ headers: { 'Content-Type': 'application/json' },
85
+ body: JSON.stringify({ executionId, action: 'step' }),
86
+ });
87
+
88
+ const data = await response.json();
89
+ if (data.error) {
90
+ console.error('Error stepping:', data.error);
91
+ }
92
+ // Don't set status here - onPause hook will send stateUpdate with actual state after step
93
+ } catch (error) {
94
+ console.error('Error stepping execution:', error);
95
+ } finally {
96
+ setIsProcessing(false);
97
+ }
98
+ };
99
+
100
+ const handleRun = () => {
101
+ if (onRun) {
102
+ onRun();
103
+ }
104
+ };
105
+
106
+ const handleStop = async () => {
107
+ if (!executionId || (status !== 'running' && status !== 'paused')) return;
108
+
109
+ setIsProcessing(true);
110
+ try {
111
+ const response = await fetch('/api/execution/controller', {
112
+ method: 'POST',
113
+ headers: { 'Content-Type': 'application/json' },
114
+ body: JSON.stringify({ executionId, action: 'stop' }),
115
+ });
116
+
117
+ const data = await response.json();
118
+ if (data.error) {
119
+ console.error('Error stopping:', data.error);
120
+ }
121
+ // Don't set status here - executionStopped event will be sent and handled in ToolTester
122
+ } catch (error) {
123
+ console.error('Error stopping execution:', error);
124
+ } finally {
125
+ setIsProcessing(false);
126
+ }
127
+ };
128
+
129
+ const handleClear = () => {
130
+ if (onClear) {
131
+ onClear();
132
+ }
133
+ };
134
+
135
+ return (
136
+ <div className={styles.container}>
137
+ <div className={styles.controlsRow}>
138
+ <div className={styles.controls}>
139
+ <button
140
+ onClick={handleRun}
141
+ disabled={disabled || isProcessing || status === 'running' || status === 'paused'}
142
+ className={styles.button}
143
+ title="Run execution"
144
+ >
145
+ ▶ Run
146
+ </button>
147
+ <button
148
+ onClick={status === 'paused' ? handleStep : (onStepFromStart || handleRun)}
149
+ disabled={isProcessing || (status !== 'paused' && status !== 'not_started' && status !== 'finished' && status !== 'error' && status !== 'stopped') || (status === 'not_started' && disabled)}
150
+ className={styles.button}
151
+ title={status === 'paused' ? 'Step to next node' : 'Start execution and pause at first node'}
152
+ >
153
+ ⏭ Step
154
+ </button>
155
+ <button
156
+ onClick={handlePause}
157
+ disabled={isProcessing || status !== 'running'}
158
+ className={styles.button}
159
+ title="Pause execution"
160
+ >
161
+ ⏸ Pause
162
+ </button>
163
+ <button
164
+ onClick={handleResume}
165
+ disabled={isProcessing || status !== 'paused'}
166
+ className={styles.button}
167
+ title="Resume execution"
168
+ >
169
+ ▶ Resume
170
+ </button>
171
+ <button
172
+ onClick={handleStop}
173
+ disabled={isProcessing || (status !== 'running' && status !== 'paused')}
174
+ className={styles.button}
175
+ title="Stop/cancel execution"
176
+ >
177
+ ⏹ Stop
178
+ </button>
179
+ <button
180
+ onClick={handleClear}
181
+ disabled={isProcessing || (status !== 'finished' && status !== 'error' && status !== 'stopped')}
182
+ className={styles.button}
183
+ title="Clear execution history and reset state"
184
+ >
185
+ × Clear
186
+ </button>
187
+ </div>
188
+
189
+ {(status === 'running' || status === 'paused' || status === 'finished' || status === 'error' || status === 'stopped') && (
190
+ <div className={styles.statusInfo}>
191
+ {currentNodeId && (
192
+ <>
193
+ <span className={styles.currentNode}>
194
+ Current: <code>{currentNodeId}</code>
195
+ </span>
196
+ <span className={styles.separator}>•</span>
197
+ </>
198
+ )}
199
+ <span className={styles.statusLabel}>Status:</span>
200
+ <span className={`${styles.statusBadge} ${styles[status]}`}>
201
+ {status.toUpperCase()}
202
+ </span>
203
+ </div>
204
+ )}
205
+ </div>
206
+ </div>
207
+ );
208
+ }
209
+
@@ -0,0 +1,268 @@
1
+ .container {
2
+ width: 100%;
3
+ height: 100%;
4
+ display: flex;
5
+ flex-direction: column;
6
+ background: white;
7
+ overflow: hidden;
8
+ }
9
+
10
+
11
+ .empty {
12
+ padding: 24px;
13
+ text-align: center;
14
+ color: #999;
15
+ font-style: italic;
16
+ }
17
+
18
+ .list {
19
+ overflow-y: auto;
20
+ flex: 1;
21
+ min-height: 0;
22
+ }
23
+
24
+ .item {
25
+ border-bottom: 1px solid #e0e0e0;
26
+ transition: background-color 0.2s;
27
+ }
28
+
29
+ .item:hover {
30
+ background-color: #f9f9f9;
31
+ }
32
+
33
+ .item.error {
34
+ border-left: 4px solid #ef5350;
35
+ }
36
+
37
+ .header {
38
+ display: flex;
39
+ align-items: center;
40
+ justify-content: space-between;
41
+ padding: 12px 16px;
42
+ cursor: pointer;
43
+ user-select: none;
44
+ }
45
+
46
+ .nodeInfo {
47
+ display: flex;
48
+ align-items: center;
49
+ gap: 8px;
50
+ flex: 1;
51
+ min-width: 0;
52
+ }
53
+
54
+ .nodeId {
55
+ font-weight: 600;
56
+ color: #333;
57
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
58
+ font-size: 13px;
59
+ }
60
+
61
+ .nodeType {
62
+ font-size: 11px;
63
+ color: #666;
64
+ background: #e8e8e8;
65
+ padding: 2px 6px;
66
+ border-radius: 4px;
67
+ text-transform: uppercase;
68
+ }
69
+
70
+ .errorBadge {
71
+ font-size: 10px;
72
+ color: white;
73
+ background: #ef5350;
74
+ padding: 2px 6px;
75
+ border-radius: 4px;
76
+ font-weight: 600;
77
+ }
78
+
79
+ .timing {
80
+ display: flex;
81
+ align-items: center;
82
+ gap: 12px;
83
+ margin-left: 12px;
84
+ }
85
+
86
+ .duration {
87
+ font-weight: 600;
88
+ color: #2196f3;
89
+ font-size: 12px;
90
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
91
+ }
92
+
93
+ .time {
94
+ font-size: 11px;
95
+ color: #999;
96
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
97
+ }
98
+
99
+ .expandButton {
100
+ background: none;
101
+ border: none;
102
+ cursor: pointer;
103
+ font-size: 10px;
104
+ color: #666;
105
+ padding: 4px 8px;
106
+ margin-left: 8px;
107
+ }
108
+
109
+ .expandButton:hover {
110
+ color: #333;
111
+ }
112
+
113
+ .details {
114
+ padding: 12px 16px;
115
+ background: #fafafa;
116
+ border-top: 1px solid #e0e0e0;
117
+ }
118
+
119
+ .errorSection {
120
+ margin-bottom: 16px;
121
+ padding: 12px;
122
+ background: #ffebee;
123
+ border-radius: 4px;
124
+ border-left: 4px solid #ef5350;
125
+ }
126
+
127
+ .errorSection strong {
128
+ display: block;
129
+ margin-bottom: 8px;
130
+ color: #c62828;
131
+ }
132
+
133
+ .errorMessage {
134
+ margin: 0;
135
+ font-size: 12px;
136
+ color: #c62828;
137
+ white-space: pre-wrap;
138
+ word-break: break-word;
139
+ }
140
+
141
+ .stackTrace {
142
+ margin-top: 8px;
143
+ font-size: 11px;
144
+ color: #999;
145
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
146
+ }
147
+
148
+ .dataSection {
149
+ display: flex;
150
+ flex-direction: column;
151
+ gap: 12px;
152
+ margin-bottom: 12px;
153
+ }
154
+
155
+ .dataItem {
156
+ display: flex;
157
+ flex-direction: column;
158
+ gap: 4px;
159
+ }
160
+
161
+ .dataItem strong {
162
+ font-size: 12px;
163
+ color: #666;
164
+ text-transform: uppercase;
165
+ letter-spacing: 0.5px;
166
+ }
167
+
168
+ .jsonData {
169
+ margin: 0;
170
+ padding: 8px;
171
+ background: white;
172
+ border: 1px solid #e0e0e0;
173
+ border-radius: 4px;
174
+ font-size: 11px;
175
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
176
+ overflow-x: auto;
177
+ max-height: 200px;
178
+ overflow-y: auto;
179
+ white-space: pre-wrap;
180
+ word-break: break-word;
181
+ }
182
+
183
+ .metadata {
184
+ display: flex;
185
+ gap: 16px;
186
+ font-size: 11px;
187
+ color: #666;
188
+ padding-top: 8px;
189
+ border-top: 1px solid #e0e0e0;
190
+ }
191
+
192
+ .metadata div {
193
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
194
+ }
195
+
196
+ /* Result item - styled to stand out at the bottom */
197
+ .resultItem {
198
+ border-top: 3px solid #4caf50;
199
+ background: linear-gradient(to bottom, #f1f8f4 0%, #ffffff 100%);
200
+ margin-top: auto;
201
+ }
202
+
203
+ .resultHeader {
204
+ display: flex;
205
+ justify-content: space-between;
206
+ align-items: center;
207
+ padding: 16px;
208
+ border-bottom: 2px solid #4caf50;
209
+ flex-wrap: wrap;
210
+ gap: 12px;
211
+ }
212
+
213
+ .resultTitle {
214
+ display: flex;
215
+ align-items: center;
216
+ gap: 8px;
217
+ font-size: 14px;
218
+ color: #2e7d32;
219
+ }
220
+
221
+ .resultIcon {
222
+ font-size: 18px;
223
+ color: #4caf50;
224
+ font-weight: bold;
225
+ }
226
+
227
+ .resultStats {
228
+ display: flex;
229
+ gap: 16px;
230
+ flex-wrap: wrap;
231
+ font-size: 12px;
232
+ }
233
+
234
+ .statItem {
235
+ color: #666;
236
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
237
+ }
238
+
239
+ .statItem strong {
240
+ color: #333;
241
+ margin-right: 4px;
242
+ }
243
+
244
+ .errorStat {
245
+ color: #d32f2f;
246
+ }
247
+
248
+ .resultContent {
249
+ padding: 16px;
250
+ background: white;
251
+ }
252
+
253
+ .resultPre {
254
+ margin: 0;
255
+ padding: 12px;
256
+ background: #fafafa;
257
+ border: 1px solid #e0e0e0;
258
+ border-radius: 4px;
259
+ font-size: 12px;
260
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
261
+ overflow-x: auto;
262
+ max-height: 300px;
263
+ overflow-y: auto;
264
+ white-space: pre-wrap;
265
+ word-break: break-word;
266
+ color: #333;
267
+ }
268
+
@@ -0,0 +1,197 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import styles from './ExecutionHistory.module.css';
5
+
6
+ export interface NodeExecutionRecord {
7
+ nodeId: string;
8
+ nodeType: string;
9
+ startTime: number;
10
+ endTime?: number; // Optional for pending/running nodes
11
+ duration?: number; // Optional for pending/running nodes
12
+ input: unknown;
13
+ output?: unknown; // Optional for pending/running nodes
14
+ error?: Error;
15
+ executionIndex?: number; // For tracking specific node instances
16
+ }
17
+
18
+ interface ExecutionHistoryProps {
19
+ history: NodeExecutionRecord[];
20
+ onNodeClick?: (nodeId: string) => void;
21
+ result?: unknown;
22
+ telemetry?: {
23
+ totalDuration: number;
24
+ nodeCounts: Record<string, number>;
25
+ errorCount: number;
26
+ };
27
+ }
28
+
29
+ export default function ExecutionHistory({ history, onNodeClick, result, telemetry }: ExecutionHistoryProps) {
30
+ const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set());
31
+
32
+ const toggleExpand = (nodeId: string) => {
33
+ const newExpanded = new Set(expandedNodes);
34
+ if (newExpanded.has(nodeId)) {
35
+ newExpanded.delete(nodeId);
36
+ } else {
37
+ newExpanded.add(nodeId);
38
+ }
39
+ setExpandedNodes(newExpanded);
40
+ };
41
+
42
+ const formatTime = (timestamp: number) => {
43
+ return new Date(timestamp).toLocaleTimeString('en-US', {
44
+ hour12: false,
45
+ hour: '2-digit',
46
+ minute: '2-digit',
47
+ second: '2-digit',
48
+ fractionalSecondDigits: 3,
49
+ });
50
+ };
51
+
52
+ const formatDuration = (ms: number) => {
53
+ if (ms < 1) return `${(ms * 1000).toFixed(0)}μs`;
54
+ if (ms < 1000) return `${ms.toFixed(2)}ms`;
55
+ return `${(ms / 1000).toFixed(2)}s`;
56
+ };
57
+
58
+ const formatJSON = (data: unknown) => {
59
+ try {
60
+ return JSON.stringify(data, null, 2);
61
+ } catch {
62
+ return String(data);
63
+ }
64
+ };
65
+
66
+ if (history.length === 0 && !result) {
67
+ return (
68
+ <div className={styles.container}>
69
+ <div className={styles.empty}>No execution history available</div>
70
+ </div>
71
+ );
72
+ }
73
+
74
+ return (
75
+ <div className={styles.container}>
76
+ <div className={styles.list}>
77
+ {history.map((record, index) => {
78
+ const isExpanded = expandedNodes.has(record.nodeId);
79
+ const hasError = !!record.error;
80
+
81
+ return (
82
+ <div
83
+ key={`${record.nodeId}-${index}`}
84
+ className={`${styles.item} ${hasError ? styles.error : ''}`}
85
+ >
86
+ <div
87
+ className={styles.header}
88
+ onClick={() => {
89
+ toggleExpand(record.nodeId);
90
+ onNodeClick?.(record.nodeId);
91
+ }}
92
+ >
93
+ <div className={styles.nodeInfo}>
94
+ <span className={styles.nodeId}>{record.nodeId}</span>
95
+ <span className={styles.nodeType}>{record.nodeType}</span>
96
+ {hasError && <span className={styles.errorBadge}>ERROR</span>}
97
+ </div>
98
+ <div className={styles.timing}>
99
+ {record.duration !== undefined ? (
100
+ <span className={styles.duration}>{formatDuration(record.duration)}</span>
101
+ ) : (
102
+ <span className={styles.duration} style={{ fontStyle: 'italic', color: '#666' }}>pending</span>
103
+ )}
104
+ <span className={styles.time}>{formatTime(record.startTime)}</span>
105
+ </div>
106
+ <button className={styles.expandButton}>
107
+ {isExpanded ? '▼' : '▶'}
108
+ </button>
109
+ </div>
110
+
111
+ {isExpanded && (
112
+ <div className={styles.details}>
113
+ {hasError && (
114
+ <div className={styles.errorSection}>
115
+ <strong>Error:</strong>
116
+ <pre className={styles.errorMessage}>
117
+ {record.error?.message || 'Unknown error'}
118
+ {record.error?.stack && (
119
+ <div className={styles.stackTrace}>
120
+ {record.error.stack}
121
+ </div>
122
+ )}
123
+ </pre>
124
+ </div>
125
+ )}
126
+
127
+ <div className={styles.dataSection}>
128
+ <div className={styles.dataItem}>
129
+ <strong>Input:</strong>
130
+ <pre className={styles.jsonData}>{formatJSON(record.input)}</pre>
131
+ </div>
132
+ {!hasError && record.output !== undefined && (
133
+ <div className={styles.dataItem}>
134
+ <strong>Output:</strong>
135
+ <pre className={styles.jsonData}>{formatJSON(record.output)}</pre>
136
+ </div>
137
+ )}
138
+ {!hasError && record.output === undefined && (
139
+ <div className={styles.dataItem}>
140
+ <strong>Output:</strong>
141
+ <div style={{ fontStyle: 'italic', color: '#666' }}>Pending execution</div>
142
+ </div>
143
+ )}
144
+ </div>
145
+
146
+ <div className={styles.metadata}>
147
+ <div>Start: {formatTime(record.startTime)}</div>
148
+ {record.endTime !== undefined && (
149
+ <div>End: {formatTime(record.endTime)}</div>
150
+ )}
151
+ {record.duration !== undefined && (
152
+ <div>Duration: {formatDuration(record.duration)}</div>
153
+ )}
154
+ {(record.endTime === undefined || record.duration === undefined) && (
155
+ <div style={{ fontStyle: 'italic', color: '#666' }}>Status: Pending</div>
156
+ )}
157
+ </div>
158
+ </div>
159
+ )}
160
+ </div>
161
+ );
162
+ })}
163
+ </div>
164
+
165
+ {/* Result display at the bottom - always expanded and styled to stand out */}
166
+ {result !== null && result !== undefined && (
167
+ <div className={styles.resultItem}>
168
+ <div className={styles.resultHeader}>
169
+ <div className={styles.resultTitle}>
170
+ <span className={styles.resultIcon}>✓</span>
171
+ <strong>Final Result</strong>
172
+ </div>
173
+ {telemetry && (
174
+ <div className={styles.resultStats}>
175
+ <span className={styles.statItem}>
176
+ <strong>Elapsed:</strong> {formatDuration(telemetry.totalDuration)}
177
+ </span>
178
+ <span className={styles.statItem}>
179
+ <strong>Nodes:</strong> {Object.values(telemetry.nodeCounts).reduce((sum, count) => sum + count, 0)}
180
+ </span>
181
+ {telemetry.errorCount > 0 && (
182
+ <span className={`${styles.statItem} ${styles.errorStat}`}>
183
+ <strong>Errors:</strong> {telemetry.errorCount}
184
+ </span>
185
+ )}
186
+ </div>
187
+ )}
188
+ </div>
189
+ <div className={styles.resultContent}>
190
+ <pre className={styles.resultPre}>{formatJSON(result)}</pre>
191
+ </div>
192
+ </div>
193
+ )}
194
+ </div>
195
+ );
196
+ }
197
+
@@ -4,3 +4,14 @@
4
4
  flex: 1;
5
5
  }
6
6
 
7
+ @keyframes pulse {
8
+ 0%, 100% {
9
+ opacity: 1;
10
+ transform: scale(1);
11
+ }
12
+ 50% {
13
+ opacity: 0.8;
14
+ transform: scale(1.02);
15
+ }
16
+ }
17
+