agent-state-machine 2.0.2 → 2.0.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.
- package/bin/cli.js +7 -3
- package/package.json +1 -1
- package/vercel-server/ui/index.html +677 -426
|
@@ -2,44 +2,112 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
|
|
4
4
|
<head>
|
|
5
|
-
<meta charset="UTF-8"
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0"
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>{{WORKFLOW_NAME}} - Remote Follow</title>
|
|
8
|
+
|
|
8
9
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
9
10
|
<script>
|
|
10
|
-
tailwind.config = {
|
|
11
|
-
darkMode: 'class',
|
|
12
|
-
}
|
|
11
|
+
tailwind.config = { darkMode: "class" };
|
|
13
12
|
</script>
|
|
13
|
+
|
|
14
14
|
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
|
|
15
15
|
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
|
|
16
16
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
|
17
|
+
|
|
17
18
|
<style>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
:root {
|
|
20
|
+
--bg: #ffffff;
|
|
21
|
+
--fg: #000000;
|
|
22
|
+
--muted: rgba(0, 0, 0, 0.6);
|
|
23
|
+
--hairline: rgba(0, 0, 0, 0.12);
|
|
24
|
+
--hairline-strong: rgba(0, 0, 0, 0.2);
|
|
25
|
+
--focus: rgba(0, 0, 0, 0.35);
|
|
26
|
+
--chip: rgba(0, 0, 0, 0.06);
|
|
27
|
+
--chip-strong: rgba(0, 0, 0, 0.1);
|
|
28
|
+
--danger: rgba(0, 0, 0, 0.85);
|
|
29
|
+
--ok: rgba(0, 0, 0, 0.85);
|
|
30
|
+
--warn: rgba(0, 0, 0, 0.85);
|
|
21
31
|
}
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
33
|
+
.dark {
|
|
34
|
+
--bg: #000000;
|
|
35
|
+
--fg: #ffffff;
|
|
36
|
+
--muted: rgba(255, 255, 255, 0.65);
|
|
37
|
+
--hairline: rgba(255, 255, 255, 0.14);
|
|
38
|
+
--hairline-strong: rgba(255, 255, 255, 0.22);
|
|
39
|
+
--focus: rgba(255, 255, 255, 0.35);
|
|
40
|
+
--chip: rgba(255, 255, 255, 0.08);
|
|
41
|
+
--chip-strong: rgba(255, 255, 255, 0.14);
|
|
42
|
+
--danger: rgba(255, 255, 255, 0.92);
|
|
43
|
+
--ok: rgba(255, 255, 255, 0.92);
|
|
44
|
+
--warn: rgba(255, 255, 255, 0.92);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
html,
|
|
48
|
+
body {
|
|
49
|
+
height: 100%;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
body {
|
|
53
|
+
background: var(--bg);
|
|
54
|
+
color: var(--fg);
|
|
55
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
56
|
+
letter-spacing: 0.01em;
|
|
57
|
+
margin: 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
::-webkit-scrollbar {
|
|
25
61
|
width: 10px;
|
|
26
62
|
height: 10px;
|
|
27
63
|
}
|
|
28
64
|
|
|
29
|
-
|
|
30
|
-
background:
|
|
65
|
+
::-webkit-scrollbar-track {
|
|
66
|
+
background: transparent;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
::-webkit-scrollbar-thumb {
|
|
70
|
+
background: var(--chip-strong);
|
|
71
|
+
border-radius: 999px;
|
|
72
|
+
border: 2px solid transparent;
|
|
73
|
+
background-clip: padding-box;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
::-webkit-scrollbar-thumb:hover {
|
|
77
|
+
background: var(--hairline-strong);
|
|
31
78
|
}
|
|
32
79
|
|
|
33
|
-
.
|
|
34
|
-
|
|
35
|
-
border-radius: 5px;
|
|
80
|
+
.hairline {
|
|
81
|
+
border: 1px solid var(--hairline);
|
|
36
82
|
}
|
|
37
83
|
|
|
38
|
-
.
|
|
39
|
-
|
|
84
|
+
.hairline-strong {
|
|
85
|
+
border: 1px solid var(--hairline-strong);
|
|
40
86
|
}
|
|
41
87
|
|
|
42
|
-
|
|
88
|
+
.divider {
|
|
89
|
+
border-top: 1px solid var(--hairline);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.kbd {
|
|
93
|
+
display: inline-flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
padding: 2px 6px;
|
|
96
|
+
border: 1px solid var(--hairline);
|
|
97
|
+
border-bottom-color: var(--hairline-strong);
|
|
98
|
+
border-radius: 8px;
|
|
99
|
+
background: transparent;
|
|
100
|
+
color: var(--muted);
|
|
101
|
+
font-size: 11px;
|
|
102
|
+
line-height: 1;
|
|
103
|
+
user-select: none;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.markdown-body {
|
|
107
|
+
white-space: pre-wrap;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@keyframes blink {
|
|
43
111
|
|
|
44
112
|
0%,
|
|
45
113
|
100% {
|
|
@@ -47,12 +115,89 @@
|
|
|
47
115
|
}
|
|
48
116
|
|
|
49
117
|
50% {
|
|
50
|
-
opacity: .
|
|
118
|
+
opacity: 0.4;
|
|
51
119
|
}
|
|
52
120
|
}
|
|
53
121
|
|
|
54
|
-
.
|
|
55
|
-
animation:
|
|
122
|
+
.blink {
|
|
123
|
+
animation: blink 1.1s steps(2, end) infinite;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.glow-dot {
|
|
127
|
+
background: var(--fg);
|
|
128
|
+
box-shadow: 0 0 0 2px var(--chip), 0 0 10px var(--fg);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
button,
|
|
132
|
+
textarea {
|
|
133
|
+
-webkit-tap-highlight-color: transparent;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
button:focus-visible,
|
|
137
|
+
textarea:focus-visible {
|
|
138
|
+
outline: 2px solid var(--focus);
|
|
139
|
+
outline-offset: 2px;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* === ONE COLUMN, "INPUT" LEFT / "OUTPUT" RIGHT, ALL LTR === */
|
|
143
|
+
.edge-wrap {
|
|
144
|
+
width: 100%;
|
|
145
|
+
padding-left: 12px;
|
|
146
|
+
padding-right: 12px;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
@media (min-width: 640px) {
|
|
150
|
+
.edge-wrap {
|
|
151
|
+
padding-left: 16px;
|
|
152
|
+
padding-right: 16px;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.io-row {
|
|
157
|
+
display: flex;
|
|
158
|
+
width: 100%;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.io-left {
|
|
162
|
+
justify-content: flex-start;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.io-right {
|
|
166
|
+
justify-content: flex-end;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.io-center {
|
|
170
|
+
justify-content: center;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.io-card {
|
|
174
|
+
width: min(880px, 100%);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.io-in {
|
|
178
|
+
text-align: left;
|
|
179
|
+
direction: ltr;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.io-out {
|
|
183
|
+
text-align: right;
|
|
184
|
+
direction: ltr;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.rtl-safe {
|
|
188
|
+
direction: ltr;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/* centered chrome (header/footer/empty states) */
|
|
192
|
+
.center-wrap {
|
|
193
|
+
width: min(880px, 100%);
|
|
194
|
+
margin-left: auto;
|
|
195
|
+
margin-right: auto;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* NEW: center the “meta rows” inside their cards */
|
|
199
|
+
.meta-center {
|
|
200
|
+
text-align: center;
|
|
56
201
|
}
|
|
57
202
|
</style>
|
|
58
203
|
</head>
|
|
@@ -61,178 +206,187 @@
|
|
|
61
206
|
<div id="root"></div>
|
|
62
207
|
|
|
63
208
|
<script>
|
|
64
|
-
|
|
65
|
-
window.
|
|
66
|
-
window.WORKFLOW_NAME_TEMPLATE = '{{WORKFLOW_NAME}}';
|
|
209
|
+
window.SESSION_TOKEN = "{{SESSION_TOKEN}}";
|
|
210
|
+
window.WORKFLOW_NAME_TEMPLATE = "{{WORKFLOW_NAME}}";
|
|
67
211
|
</script>
|
|
68
212
|
|
|
69
213
|
<script type="text/babel">
|
|
70
|
-
const {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
214
|
+
const { useEffect, useMemo, useState } = React;
|
|
215
|
+
|
|
216
|
+
const Icon = ({ name }) => {
|
|
217
|
+
const common = "w-4 h-4";
|
|
218
|
+
if (name === "sun") {
|
|
219
|
+
return (
|
|
220
|
+
<svg className={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
221
|
+
<path strokeLinecap="round" d="M12 3v2M12 19v2M4 12H2M22 12h-2M5.6 5.6l1.4 1.4M17 17l1.4 1.4M18.4 5.6 17 7M7 17l-1.4 1.4" />
|
|
222
|
+
<circle cx="12" cy="12" r="4" />
|
|
223
|
+
</svg>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
if (name === "moon") {
|
|
227
|
+
return (
|
|
228
|
+
<svg className={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
229
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M21 13.2A8.5 8.5 0 0 1 10.8 3 7.5 7.5 0 1 0 21 13.2z" />
|
|
230
|
+
</svg>
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
if (name === "copy") {
|
|
234
|
+
return (
|
|
235
|
+
<svg className={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
236
|
+
<rect x="9" y="9" width="13" height="13" rx="2" />
|
|
237
|
+
<path strokeLinecap="round" d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
238
|
+
</svg>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (name === "check") {
|
|
242
|
+
return (
|
|
243
|
+
<svg className={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
244
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M20 6 9 17l-5-5" />
|
|
245
|
+
</svg>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
if (name === "sort") {
|
|
249
|
+
return (
|
|
250
|
+
<svg className={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
251
|
+
<path strokeLinecap="round" d="M7 4h14M7 8h10M7 12h6" />
|
|
252
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M3 20V4m0 16 2-2m-2 2-2-2" />
|
|
253
|
+
</svg>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
};
|
|
96
258
|
|
|
97
259
|
function StatusBadge({ status }) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
connecting: 'bg-yellow-500 animate-pulse-slow',
|
|
102
|
-
};
|
|
103
|
-
const labels = {
|
|
104
|
-
connected: 'Live',
|
|
105
|
-
disconnected: 'Offline',
|
|
106
|
-
connecting: 'Connecting...',
|
|
107
|
-
};
|
|
260
|
+
const labels = { connected: "Live", disconnected: "Offline", connecting: "Connecting..." };
|
|
261
|
+
const dotClass =
|
|
262
|
+
status === "connecting" ? "blink" : status === "connected" ? "blink glow-dot" : "";
|
|
108
263
|
return (
|
|
109
264
|
<div className="flex items-center gap-2">
|
|
110
|
-
<div className={`w-2 h-2 rounded-full ${
|
|
111
|
-
<span className="text-[10px] uppercase tracking-wider
|
|
265
|
+
<div className={`w-2 h-2 rounded-full ${dotClass}`} />
|
|
266
|
+
<span className="text-[10px] uppercase tracking-wider font-bold" style={{ color: "var(--muted)" }}>
|
|
267
|
+
{labels[status] || status}
|
|
268
|
+
</span>
|
|
112
269
|
</div>
|
|
113
270
|
);
|
|
114
271
|
}
|
|
115
272
|
|
|
116
|
-
function CopyButton({ text, className }) {
|
|
273
|
+
function CopyButton({ text, className = "" }) {
|
|
117
274
|
const [copied, setCopied] = useState(false);
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
275
|
+
const handleCopy = async () => {
|
|
276
|
+
const content = typeof text === "object" ? JSON.stringify(text, null, 2) : String(text);
|
|
277
|
+
try {
|
|
278
|
+
await navigator.clipboard.writeText(content);
|
|
279
|
+
setCopied(true);
|
|
280
|
+
setTimeout(() => setCopied(false), 1500);
|
|
281
|
+
} catch (e) {
|
|
282
|
+
const ta = document.createElement("textarea");
|
|
283
|
+
ta.value = content;
|
|
284
|
+
document.body.appendChild(ta);
|
|
285
|
+
ta.select();
|
|
286
|
+
document.execCommand("copy");
|
|
287
|
+
document.body.removeChild(ta);
|
|
288
|
+
setCopied(true);
|
|
289
|
+
setTimeout(() => setCopied(false), 1500);
|
|
290
|
+
}
|
|
124
291
|
};
|
|
125
|
-
|
|
126
292
|
return (
|
|
127
293
|
<button
|
|
128
294
|
onClick={handleCopy}
|
|
129
|
-
className={`flex items-center
|
|
130
|
-
title="Copy
|
|
295
|
+
className={`inline-flex items-center gap-2 px-3 py-2 rounded-xl hairline hover:hairline-strong transition ${className}`}
|
|
296
|
+
title="Copy"
|
|
297
|
+
type="button"
|
|
298
|
+
style={{ color: "var(--fg)", background: "transparent" }}
|
|
131
299
|
>
|
|
132
|
-
{copied ? <
|
|
133
|
-
<span>{copied ?
|
|
300
|
+
{copied ? <Icon name="check" /> : <Icon name="copy" />}
|
|
301
|
+
<span className="text-[11px] tracking-[0.14em] font-semibold">{copied ? "COPIED" : "COPY"}</span>
|
|
134
302
|
</button>
|
|
135
303
|
);
|
|
136
304
|
}
|
|
137
305
|
|
|
138
|
-
function
|
|
139
|
-
|
|
306
|
+
function Toggle({ onClick, label, title, children }) {
|
|
307
|
+
return (
|
|
308
|
+
<button
|
|
309
|
+
onClick={onClick}
|
|
310
|
+
className="inline-flex items-center gap-2 px-3 py-2 rounded-xl hairline hover:hairline-strong transition"
|
|
311
|
+
title={title}
|
|
312
|
+
type="button"
|
|
313
|
+
style={{ background: "transparent", color: "var(--fg)" }}
|
|
314
|
+
>
|
|
315
|
+
{children}
|
|
316
|
+
<span className="text-[11px] tracking-[0.14em] font-semibold">{label}</span>
|
|
317
|
+
</button>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
140
320
|
|
|
141
|
-
|
|
321
|
+
function JsonView({ data, label, onTop = false, timestamp, align = "left" }) {
|
|
322
|
+
const [viewMode, setViewMode] = useState("clean"); // clean | raw
|
|
323
|
+
const isObject = typeof data === "object" && data !== null;
|
|
142
324
|
const rawContent = isObject ? JSON.stringify(data, null, 2) : String(data);
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
<div key={k} className="mb-5 last:mb-0">
|
|
157
|
-
<div className="text-[11px] font-extrabold uppercase tracking-wider text-blue-600 dark:text-blue-400 mb-2">
|
|
158
|
-
{k}
|
|
159
|
-
</div>
|
|
160
|
-
<div className="whitespace-pre-wrap leading-relaxed">
|
|
161
|
-
{prettyVal}
|
|
162
|
-
</div>
|
|
325
|
+
const hasToggle = isObject || rawContent.includes("\\n");
|
|
326
|
+
|
|
327
|
+
const cleanParts = useMemo(() => {
|
|
328
|
+
if (!isObject) return null;
|
|
329
|
+
const keys = Object.keys(data || {});
|
|
330
|
+
if (!keys.length) return null;
|
|
331
|
+
return keys.map((k) => {
|
|
332
|
+
const val = data[k];
|
|
333
|
+
const renderedVal = typeof val === "string" ? val : JSON.stringify(val, null, 2);
|
|
334
|
+
return (
|
|
335
|
+
<div key={k} className="py-4">
|
|
336
|
+
<div className="text-[11px] tracking-[0.18em] font-semibold mb-2" style={{ color: "var(--muted)" }}>
|
|
337
|
+
{k.toUpperCase()}
|
|
163
338
|
</div>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const rawContentUnescaped = rawContent;
|
|
170
|
-
const hasToggle = isObject || rawContent.includes('\\n');
|
|
339
|
+
<div className="markdown-body text-[13px] leading-relaxed">{String(renderedVal)}</div>
|
|
340
|
+
</div>
|
|
341
|
+
);
|
|
342
|
+
});
|
|
343
|
+
}, [data, isObject]);
|
|
171
344
|
|
|
172
|
-
|
|
173
|
-
return (
|
|
174
|
-
<div className="flex justify-end w-full group">
|
|
175
|
-
<div className="max-w-[85%] bg-blue-50 dark:bg-blue-950/20 border border-blue-100 dark:border-blue-900/40 rounded-2xl rounded-tr-none shadow-sm p-6 transition-all hover:border-blue-200 dark:hover:border-blue-800 relative">
|
|
176
|
-
<div className="flex justify-between items-center mb-3">
|
|
177
|
-
<div className="text-[9px] font-black text-blue-300 dark:text-blue-800/60 uppercase tracking-[0.2em] text-right w-full">
|
|
178
|
-
{label}
|
|
179
|
-
</div>
|
|
180
|
-
<div className="absolute top-4 left-4 opacity-0 group-hover:opacity-100 transition-opacity flex items-center space-x-2">
|
|
181
|
-
{hasToggle && (
|
|
182
|
-
<button
|
|
183
|
-
onClick={() => setViewMode(v => v === 'clean' ? 'raw' : 'clean')}
|
|
184
|
-
className="text-[9px] uppercase tracking-wider font-bold text-blue-400 hover:text-blue-600 dark:text-blue-600 dark:hover:text-blue-400 focus:outline-none"
|
|
185
|
-
>
|
|
186
|
-
{viewMode === 'clean' ? 'Raw' : 'Clean'}
|
|
187
|
-
</button>
|
|
188
|
-
)}
|
|
189
|
-
<CopyButton text={data} className="text-blue-400 hover:text-blue-600 dark:text-blue-600 dark:hover:text-blue-400" />
|
|
190
|
-
</div>
|
|
191
|
-
</div>
|
|
345
|
+
const ioClass = align === "right" ? "io-out" : "io-in";
|
|
192
346
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
347
|
+
return (
|
|
348
|
+
<div className={`hairline rounded-2xl ${onTop ? "rounded-tr-none" : ""} overflow-hidden ${ioClass}`} style={{ background: "transparent" }}>
|
|
349
|
+
<div className="rtl-safe flex items-center justify-between px-5 py-4 divider">
|
|
350
|
+
<div className="flex items-center gap-3 min-w-0">
|
|
351
|
+
<div className="text-[11px] tracking-[0.22em] font-semibold truncate" style={{ color: "var(--muted)" }}>
|
|
352
|
+
{label}
|
|
198
353
|
</div>
|
|
354
|
+
{timestamp && (
|
|
355
|
+
<span className="text-[11px] tracking-[0.12em]" style={{ color: "var(--muted)" }}>
|
|
356
|
+
{timestamp}
|
|
357
|
+
</span>
|
|
358
|
+
)}
|
|
199
359
|
</div>
|
|
200
|
-
</div>
|
|
201
|
-
);
|
|
202
|
-
}
|
|
203
360
|
|
|
204
|
-
|
|
205
|
-
<div className="bg-gray-100 dark:bg-zinc-900/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-4 py-3 text-xs w-full max-w-2xl font-mono overflow-x-auto relative group">
|
|
206
|
-
<div className="text-[9px] text-gray-400 dark:text-zinc-600 uppercase tracking-widest mb-1 flex justify-between items-center">
|
|
207
|
-
<div className="flex space-x-4">
|
|
208
|
-
<span>{label}</span>
|
|
209
|
-
{timestamp && <span>{timestamp}</span>}
|
|
210
|
-
</div>
|
|
211
|
-
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex items-center space-x-2">
|
|
361
|
+
<div className="flex items-center gap-2">
|
|
212
362
|
{hasToggle && (
|
|
213
363
|
<button
|
|
214
|
-
onClick={() => setViewMode(v => v ===
|
|
215
|
-
className="
|
|
364
|
+
onClick={() => setViewMode((v) => (v === "clean" ? "raw" : "clean"))}
|
|
365
|
+
className="px-3 py-2 rounded-xl hairline hover:hairline-strong transition"
|
|
366
|
+
type="button"
|
|
367
|
+
title={viewMode === "clean" ? "Switch to raw" : "Switch to clean"}
|
|
368
|
+
style={{ background: "transparent", color: "var(--fg)" }}
|
|
216
369
|
>
|
|
217
|
-
|
|
370
|
+
<span className="text-[11px] tracking-[0.14em] font-semibold">
|
|
371
|
+
{viewMode === "clean" ? "RAW" : "CLEAN"}
|
|
372
|
+
</span>
|
|
218
373
|
</button>
|
|
219
374
|
)}
|
|
220
|
-
<CopyButton text={data}
|
|
375
|
+
<CopyButton text={data} />
|
|
221
376
|
</div>
|
|
222
377
|
</div>
|
|
223
378
|
|
|
224
|
-
<div className="
|
|
225
|
-
|
|
226
|
-
? (cleanParts ??
|
|
227
|
-
|
|
228
|
-
}
|
|
379
|
+
<div className="px-5 py-4">
|
|
380
|
+
<div className="markdown-body text-[13px] leading-relaxed overflow-x-auto">
|
|
381
|
+
{viewMode === "clean" ? (cleanParts ?? rawContent) : rawContent}
|
|
382
|
+
</div>
|
|
229
383
|
</div>
|
|
230
384
|
</div>
|
|
231
385
|
);
|
|
232
386
|
}
|
|
233
387
|
|
|
234
388
|
function InteractionForm({ interaction, onSubmit, disabled }) {
|
|
235
|
-
const [response, setResponse] = useState(
|
|
389
|
+
const [response, setResponse] = useState("");
|
|
236
390
|
const [submitting, setSubmitting] = useState(false);
|
|
237
391
|
|
|
238
392
|
const handleSubmit = async (e) => {
|
|
@@ -241,41 +395,60 @@
|
|
|
241
395
|
setSubmitting(true);
|
|
242
396
|
try {
|
|
243
397
|
await onSubmit(interaction.slug, interaction.targetKey, response.trim());
|
|
244
|
-
setResponse(
|
|
398
|
+
setResponse("");
|
|
245
399
|
} finally {
|
|
246
400
|
setSubmitting(false);
|
|
247
401
|
}
|
|
248
402
|
};
|
|
249
403
|
|
|
250
404
|
return (
|
|
251
|
-
<
|
|
252
|
-
<div className="
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
405
|
+
<section className="hairline rounded-2xl overflow-hidden io-in">
|
|
406
|
+
<div className="rtl-safe px-6 py-5 divider flex items-center justify-between">
|
|
407
|
+
<div className="min-w-0">
|
|
408
|
+
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
409
|
+
INPUT REQUIRED
|
|
410
|
+
</div>
|
|
411
|
+
<div className="mt-2 text-[13px] leading-relaxed markdown-body">
|
|
412
|
+
{interaction.question || "Please provide your input."}
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
<div className="flex items-center gap-2">
|
|
416
|
+
{disabled && <span className="kbd" title="CLI offline">OFFLINE</span>}
|
|
417
|
+
</div>
|
|
258
418
|
</div>
|
|
259
|
-
|
|
419
|
+
|
|
420
|
+
<form onSubmit={handleSubmit} className="px-6 py-5">
|
|
260
421
|
<textarea
|
|
261
422
|
value={response}
|
|
262
423
|
onChange={(e) => setResponse(e.target.value)}
|
|
263
|
-
|
|
264
|
-
placeholder="Type
|
|
265
|
-
|
|
424
|
+
disabled={disabled || submitting}
|
|
425
|
+
placeholder="Type response…"
|
|
426
|
+
className="w-full rounded-2xl hairline p-4 text-[13px] leading-relaxed min-h-[120px]"
|
|
427
|
+
style={{
|
|
428
|
+
background: "transparent",
|
|
429
|
+
color: "var(--fg)",
|
|
430
|
+
borderColor: "var(--hairline)",
|
|
431
|
+
outline: "none",
|
|
432
|
+
resize: "vertical",
|
|
433
|
+
direction: "ltr",
|
|
434
|
+
textAlign: "left",
|
|
435
|
+
}}
|
|
266
436
|
/>
|
|
267
|
-
<div className="flex
|
|
268
|
-
|
|
437
|
+
<div className="mt-4 flex items-center justify-between gap-3 rtl-safe">
|
|
438
|
+
<div className="text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
|
|
439
|
+
{submitting ? "SENDING…" : " "}
|
|
440
|
+
</div>
|
|
269
441
|
<button
|
|
270
442
|
type="submit"
|
|
271
|
-
disabled={
|
|
272
|
-
className="px-
|
|
443
|
+
disabled={disabled || submitting || !response.trim()}
|
|
444
|
+
className="px-4 py-2 rounded-xl hairline hover:hairline-strong transition disabled:opacity-40 disabled:cursor-not-allowed"
|
|
445
|
+
style={{ background: "transparent", color: "var(--fg)" }}
|
|
273
446
|
>
|
|
274
|
-
|
|
447
|
+
<span className="text-[11px] tracking-[0.18em] font-semibold">SUBMIT</span>
|
|
275
448
|
</button>
|
|
276
449
|
</div>
|
|
277
450
|
</form>
|
|
278
|
-
</
|
|
451
|
+
</section>
|
|
279
452
|
);
|
|
280
453
|
}
|
|
281
454
|
|
|
@@ -283,39 +456,39 @@
|
|
|
283
456
|
const [history, setHistory] = useState([]);
|
|
284
457
|
const [loading, setLoading] = useState(true);
|
|
285
458
|
const [error, setError] = useState(null);
|
|
286
|
-
const [status, setStatus] = useState(
|
|
287
|
-
const [workflowName, setWorkflowName] = useState(window.WORKFLOW_NAME_TEMPLATE ||
|
|
288
|
-
const [theme, setTheme] = useState(
|
|
459
|
+
const [status, setStatus] = useState("connecting");
|
|
460
|
+
const [workflowName, setWorkflowName] = useState(window.WORKFLOW_NAME_TEMPLATE || "");
|
|
461
|
+
const [theme, setTheme] = useState(() => {
|
|
462
|
+
const saved = localStorage.getItem("rf_theme");
|
|
463
|
+
if (saved === "light" || saved === "dark") return saved;
|
|
464
|
+
return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
465
|
+
});
|
|
289
466
|
const [sortNewest, setSortNewest] = useState(true);
|
|
290
467
|
const [pendingInteraction, setPendingInteraction] = useState(null);
|
|
291
468
|
|
|
292
|
-
const token =
|
|
469
|
+
const token =
|
|
470
|
+
window.SESSION_TOKEN && window.SESSION_TOKEN !== "{{" + "SESSION_TOKEN" + "}}" ? window.SESSION_TOKEN : null;
|
|
471
|
+
|
|
472
|
+
const historyUrl = token ? `/api/history/${token}` : "/api/history";
|
|
473
|
+
const eventsUrl = token ? `/api/events/${token}` : "/api/events";
|
|
474
|
+
const submitUrl = token ? `/api/submit/${token}` : "/api/submit";
|
|
293
475
|
|
|
294
|
-
|
|
295
|
-
const historyUrl = token ? `/api/history/${token}` : '/api/history';
|
|
296
|
-
const eventsUrl = token ? `/api/events/${token}` : '/api/events';
|
|
297
|
-
const submitUrl = token ? `/api/submit/${token}` : '/api/submit';
|
|
476
|
+
useEffect(() => localStorage.setItem("rf_theme", theme), [theme]);
|
|
298
477
|
|
|
299
|
-
// Detect pending interactions
|
|
300
478
|
useEffect(() => {
|
|
301
|
-
if (history.length === 0) {
|
|
302
|
-
setPendingInteraction(null);
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
479
|
+
if (history.length === 0) { setPendingInteraction(null); return; }
|
|
305
480
|
|
|
306
481
|
const resolvedSlugs = new Set();
|
|
307
482
|
let pending = null;
|
|
308
483
|
|
|
309
484
|
for (const entry of history) {
|
|
310
|
-
const isResolution =
|
|
311
|
-
entry.event ===
|
|
312
|
-
entry.event ===
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (isResolution && entry.slug)
|
|
317
|
-
resolvedSlugs.add(entry.slug);
|
|
318
|
-
}
|
|
485
|
+
const isResolution =
|
|
486
|
+
entry.event === "INTERACTION_RESOLVED" ||
|
|
487
|
+
entry.event === "PROMPT_ANSWERED" ||
|
|
488
|
+
entry.event === "INTERACTION_SUBMITTED";
|
|
489
|
+
const isRequest = entry.event === "INTERACTION_REQUESTED" || entry.event === "PROMPT_REQUESTED";
|
|
490
|
+
|
|
491
|
+
if (isResolution && entry.slug) resolvedSlugs.add(entry.slug);
|
|
319
492
|
|
|
320
493
|
if (isRequest && entry.slug && !resolvedSlugs.has(entry.slug) && !pending) {
|
|
321
494
|
pending = {
|
|
@@ -336,13 +509,8 @@
|
|
|
336
509
|
if (data.entries) setHistory(data.entries);
|
|
337
510
|
if (data.workflowName) setWorkflowName(data.workflowName);
|
|
338
511
|
|
|
339
|
-
|
|
340
|
-
if (token
|
|
341
|
-
setStatus(data.cliConnected ? 'connected' : 'disconnected');
|
|
342
|
-
} else if (!token) {
|
|
343
|
-
// Local mode is always "connected" if the page loads
|
|
344
|
-
setStatus('connected');
|
|
345
|
-
}
|
|
512
|
+
if (token && data.cliConnected !== undefined) setStatus(data.cliConnected ? "connected" : "disconnected");
|
|
513
|
+
else if (!token) setStatus("connected");
|
|
346
514
|
|
|
347
515
|
setLoading(false);
|
|
348
516
|
return true;
|
|
@@ -366,13 +534,13 @@
|
|
|
366
534
|
eventSource = new EventSource(eventsUrl);
|
|
367
535
|
|
|
368
536
|
eventSource.onopen = () => {
|
|
369
|
-
setStatus(
|
|
537
|
+
setStatus("connected");
|
|
370
538
|
reconnectAttempts = 0;
|
|
371
539
|
fetchData();
|
|
372
540
|
};
|
|
373
541
|
|
|
374
542
|
eventSource.onerror = () => {
|
|
375
|
-
setStatus(
|
|
543
|
+
setStatus("disconnected");
|
|
376
544
|
eventSource.close();
|
|
377
545
|
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000);
|
|
378
546
|
reconnectAttempts++;
|
|
@@ -381,34 +549,34 @@
|
|
|
381
549
|
|
|
382
550
|
eventSource.onmessage = (e) => {
|
|
383
551
|
try {
|
|
384
|
-
if (e.data ===
|
|
385
|
-
fetchData();
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
|
|
552
|
+
if (e.data === "update") { fetchData(); return; }
|
|
389
553
|
const data = JSON.parse(e.data);
|
|
390
554
|
switch (data.type) {
|
|
391
|
-
case
|
|
392
|
-
setStatus(data.cliConnected ?
|
|
555
|
+
case "status":
|
|
556
|
+
setStatus(data.cliConnected ? "connected" : "disconnected");
|
|
393
557
|
if (data.workflowName) setWorkflowName(data.workflowName);
|
|
394
558
|
break;
|
|
395
|
-
case
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
);
|
|
559
|
+
case "history":
|
|
560
|
+
setHistory(data.entries || []);
|
|
561
|
+
break;
|
|
562
|
+
case "event":
|
|
563
|
+
setHistory((prev) => {
|
|
564
|
+
if (data.event === "INTERACTION_SUBMITTED" && data.slug) {
|
|
565
|
+
const hasDupe = prev.some((e) => e.event === "INTERACTION_SUBMITTED" && e.slug === data.slug);
|
|
402
566
|
if (hasDupe) return prev;
|
|
403
567
|
}
|
|
404
568
|
return [data, ...prev];
|
|
405
569
|
});
|
|
406
570
|
break;
|
|
407
|
-
case
|
|
408
|
-
case
|
|
409
|
-
|
|
571
|
+
case "cli_connected":
|
|
572
|
+
case "cli_reconnected":
|
|
573
|
+
setStatus("connected");
|
|
574
|
+
break;
|
|
575
|
+
case "cli_disconnected":
|
|
576
|
+
setStatus("disconnected");
|
|
577
|
+
break;
|
|
410
578
|
}
|
|
411
|
-
} catch (err) {
|
|
579
|
+
} catch (err) { }
|
|
412
580
|
};
|
|
413
581
|
};
|
|
414
582
|
|
|
@@ -425,254 +593,337 @@
|
|
|
425
593
|
const handleSubmit = async (slug, targetKey, response) => {
|
|
426
594
|
const optimisticEvent = {
|
|
427
595
|
timestamp: new Date().toISOString(),
|
|
428
|
-
event:
|
|
596
|
+
event: "INTERACTION_SUBMITTED",
|
|
429
597
|
slug,
|
|
430
598
|
targetKey,
|
|
431
|
-
answer: response.substring(0, 200) + (response.length > 200 ?
|
|
432
|
-
source:
|
|
599
|
+
answer: response.substring(0, 200) + (response.length > 200 ? "..." : ""),
|
|
600
|
+
source: "remote",
|
|
433
601
|
};
|
|
434
|
-
setHistory(prev => [optimisticEvent, ...prev]);
|
|
602
|
+
setHistory((prev) => [optimisticEvent, ...prev]);
|
|
435
603
|
|
|
436
604
|
try {
|
|
437
605
|
const res = await fetch(submitUrl, {
|
|
438
|
-
method:
|
|
439
|
-
headers: {
|
|
606
|
+
method: "POST",
|
|
607
|
+
headers: { "Content-Type": "application/json" },
|
|
440
608
|
body: JSON.stringify({ slug, targetKey, response }),
|
|
441
609
|
});
|
|
610
|
+
|
|
442
611
|
if (!res.ok) {
|
|
443
|
-
setHistory(prev => prev.filter(e => e !== optimisticEvent));
|
|
444
|
-
const
|
|
445
|
-
throw new Error(
|
|
612
|
+
setHistory((prev) => prev.filter((e) => e !== optimisticEvent));
|
|
613
|
+
const err = await res.json();
|
|
614
|
+
throw new Error(err.error || "Failed to submit");
|
|
446
615
|
}
|
|
616
|
+
|
|
447
617
|
setTimeout(fetchData, 1000);
|
|
448
618
|
} catch (err) {
|
|
449
|
-
setHistory(prev => prev.filter(e => e !== optimisticEvent));
|
|
619
|
+
setHistory((prev) => prev.filter((e) => e !== optimisticEvent));
|
|
450
620
|
alert(err.message);
|
|
451
621
|
}
|
|
452
622
|
};
|
|
453
623
|
|
|
454
|
-
const toggleTheme = () => setTheme(
|
|
455
|
-
const toggleSort = () => setSortNewest(
|
|
624
|
+
const toggleTheme = () => setTheme((p) => (p === "dark" ? "light" : "dark"));
|
|
625
|
+
const toggleSort = () => setSortNewest((p) => !p);
|
|
456
626
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
627
|
+
const formatTime = (ts) =>
|
|
628
|
+
new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
|
|
629
|
+
|
|
630
|
+
const visibleEvents = sortNewest ? history : [...history].reverse();
|
|
631
|
+
|
|
632
|
+
const wrapIO = (side, children, key) => (
|
|
633
|
+
<div key={key} className={`io-row ${side === "right" ? "io-right" : side === "center" ? "io-center" : "io-left"}`}>
|
|
634
|
+
<div className="io-card">{children}</div>
|
|
462
635
|
</div>
|
|
463
636
|
);
|
|
464
637
|
|
|
465
|
-
|
|
466
|
-
|
|
638
|
+
const renderPromptInputCard = (prompt, key) =>
|
|
639
|
+
wrapIO(
|
|
640
|
+
"left",
|
|
641
|
+
<section className="hairline rounded-2xl rounded-tl-none overflow-hidden io-in">
|
|
642
|
+
<div className="rtl-safe px-5 py-4 divider flex items-center justify-between">
|
|
643
|
+
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
644
|
+
PROMPT / INPUT
|
|
645
|
+
</div>
|
|
646
|
+
<CopyButton text={prompt} />
|
|
647
|
+
</div>
|
|
648
|
+
<div className="px-5 py-4">
|
|
649
|
+
<div className="markdown-body text-[13px] leading-relaxed overflow-x-auto">{prompt}</div>
|
|
650
|
+
</div>
|
|
651
|
+
</section>,
|
|
652
|
+
key
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
const renderEvent = (item, idx) => {
|
|
656
|
+
const time = formatTime(item.timestamp);
|
|
657
|
+
|
|
658
|
+
if (item.event && item.event.startsWith("WORKFLOW_")) {
|
|
659
|
+
return wrapIO(
|
|
660
|
+
"center",
|
|
661
|
+
<section className="text-center">
|
|
662
|
+
<div className="divider" />
|
|
663
|
+
<div className="py-4">
|
|
664
|
+
<div className="text-[11px] tracking-[0.24em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
665
|
+
{item.event.replace("WORKFLOW_", "")} • {time}
|
|
666
|
+
</div>
|
|
667
|
+
{item.error && <div className="mt-3 text-[13px] leading-relaxed markdown-body">{item.error}</div>}
|
|
668
|
+
</div>
|
|
669
|
+
<div className="divider" />
|
|
670
|
+
</section>,
|
|
671
|
+
idx
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// CENTERED inside card
|
|
676
|
+
if (item.event === "AGENT_STARTED") {
|
|
677
|
+
return wrapIO(
|
|
678
|
+
"center",
|
|
679
|
+
<section className="meta-center">
|
|
680
|
+
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
681
|
+
AGENT STARTED
|
|
682
|
+
</div>
|
|
683
|
+
<div className="mt-2 text-[13px] leading-relaxed">
|
|
684
|
+
<span className="font-semibold">{item.agent}</span>
|
|
685
|
+
</div>
|
|
686
|
+
<div className="mt-1 text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
|
|
687
|
+
{time}
|
|
688
|
+
</div>
|
|
689
|
+
</section>,
|
|
690
|
+
idx
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (item.event === "AGENT_FAILED") {
|
|
695
|
+
return wrapIO(
|
|
696
|
+
"center",
|
|
697
|
+
<section className="hairline rounded-2xl overflow-hidden">
|
|
698
|
+
<div className="rtl-safe px-5 py-4 divider flex items-center justify-between">
|
|
699
|
+
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
700
|
+
AGENT FAILED
|
|
701
|
+
</div>
|
|
702
|
+
<span className="text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
|
|
703
|
+
{time}
|
|
704
|
+
</span>
|
|
705
|
+
</div>
|
|
706
|
+
<div className="px-5 py-4 meta-center">
|
|
707
|
+
<div className="text-[13px] leading-relaxed"><span className="font-semibold">{item.agent}</span></div>
|
|
708
|
+
<div className="mt-3 text-[13px] leading-relaxed markdown-body">{item.error}</div>
|
|
709
|
+
</div>
|
|
710
|
+
</section>,
|
|
711
|
+
idx
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (item.event === "INTERACTION_REQUESTED" || item.event === "PROMPT_REQUESTED") {
|
|
716
|
+
return wrapIO(
|
|
717
|
+
"left",
|
|
718
|
+
<section className="hairline rounded-2xl px-6 py-5 io-in">
|
|
719
|
+
<div className="rtl-safe flex items-center justify-between gap-4">
|
|
720
|
+
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
721
|
+
INTERVENTION REQUIRED
|
|
722
|
+
</div>
|
|
723
|
+
<span className="text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
|
|
724
|
+
{time}
|
|
725
|
+
</span>
|
|
726
|
+
</div>
|
|
727
|
+
<div className="mt-3 text-[13px] leading-relaxed markdown-body">
|
|
728
|
+
{item.question ? `"${item.question}"` : `Waiting for response to "${item.slug}"…`}
|
|
729
|
+
</div>
|
|
730
|
+
</section>,
|
|
731
|
+
idx
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
if (item.event === "PROMPT_ANSWERED" || item.event === "INTERACTION_SUBMITTED") {
|
|
736
|
+
const isManual = item.source === "remote";
|
|
737
|
+
return wrapIO(
|
|
738
|
+
"left",
|
|
739
|
+
<section className="hairline rounded-2xl px-6 py-5 io-in">
|
|
740
|
+
<div className="rtl-safe flex items-center justify-between gap-4">
|
|
741
|
+
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
742
|
+
{isManual ? "RESOLVED VIA BROWSER" : "USER ANSWERED"}
|
|
743
|
+
</div>
|
|
744
|
+
<span className="text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
|
|
745
|
+
{time}
|
|
746
|
+
</span>
|
|
747
|
+
</div>
|
|
748
|
+
<div className="mt-3 text-[13px] leading-relaxed markdown-body">"{item.answer}"</div>
|
|
749
|
+
</section>,
|
|
750
|
+
idx
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
if (item.event === "AGENT_COMPLETED" || item.event === "INTERACTION_RESOLVED") {
|
|
755
|
+
return (
|
|
756
|
+
<React.Fragment key={idx}>
|
|
757
|
+
{wrapIO(
|
|
758
|
+
"center",
|
|
759
|
+
<section className="meta-center">
|
|
760
|
+
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
761
|
+
DONE
|
|
762
|
+
</div>
|
|
763
|
+
<div className="mt-1 text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
|
|
764
|
+
{(item.agent || item.slug || "").toString()} • {time}
|
|
765
|
+
</div>
|
|
766
|
+
</section>,
|
|
767
|
+
`done-meta-${idx}`
|
|
768
|
+
)}
|
|
769
|
+
|
|
770
|
+
{(item.output || item.result) && wrapIO(
|
|
771
|
+
"right",
|
|
772
|
+
<section className="io-out">
|
|
773
|
+
<JsonView data={item.output || item.result} label="OUTPUT / RESPONSE" align="right" />
|
|
774
|
+
</section>,
|
|
775
|
+
`done-io-${idx}`
|
|
776
|
+
)}
|
|
777
|
+
|
|
778
|
+
{item.prompt ? renderPromptInputCard(item.prompt, `prompt-${idx}`) : null}
|
|
779
|
+
</React.Fragment>
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const stripped = JSON.parse(
|
|
784
|
+
JSON.stringify(item, (key, value) => {
|
|
785
|
+
if (key === "event" || key === "timestamp") return undefined;
|
|
786
|
+
return value;
|
|
787
|
+
})
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
if (item.prompt) {
|
|
791
|
+
return (
|
|
792
|
+
<React.Fragment key={idx}>
|
|
793
|
+
{wrapIO(
|
|
794
|
+
"center",
|
|
795
|
+
<section>
|
|
796
|
+
<JsonView
|
|
797
|
+
data={stripped}
|
|
798
|
+
label={(item.event || "EVENT").toString()}
|
|
799
|
+
timestamp={time}
|
|
800
|
+
align="left"
|
|
801
|
+
/>
|
|
802
|
+
</section>,
|
|
803
|
+
`evt-${idx}`
|
|
804
|
+
)}
|
|
805
|
+
{renderPromptInputCard(item.prompt, `prompt-${idx}`)}
|
|
806
|
+
</React.Fragment>
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return wrapIO(
|
|
811
|
+
"center",
|
|
812
|
+
<section>
|
|
813
|
+
<JsonView
|
|
814
|
+
data={stripped}
|
|
815
|
+
label={(item.event || "EVENT").toString()}
|
|
816
|
+
timestamp={time}
|
|
817
|
+
align="left"
|
|
818
|
+
/>
|
|
819
|
+
</section>,
|
|
820
|
+
idx
|
|
821
|
+
);
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
if (loading && !history.length) {
|
|
825
|
+
return (
|
|
826
|
+
<div className={theme}>
|
|
827
|
+
<div className="min-h-screen flex items-center justify-center">
|
|
828
|
+
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
829
|
+
OPENING TERMINAL<span className="blink">…</span>
|
|
830
|
+
</div>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
);
|
|
834
|
+
}
|
|
467
835
|
|
|
468
836
|
return (
|
|
469
837
|
<div className={theme}>
|
|
470
|
-
<div className="min-h-screen
|
|
471
|
-
<div className="
|
|
472
|
-
|
|
473
|
-
{
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
838
|
+
<div className="min-h-screen" style={{ background: "var(--bg)", color: "var(--fg)" }}>
|
|
839
|
+
<div className="edge-wrap">
|
|
840
|
+
{/* Header (centered) */}
|
|
841
|
+
<header className="sticky top-0 z-50 py-4" style={{ background: "var(--bg)" }}>
|
|
842
|
+
<div className="center-wrap">
|
|
843
|
+
<div className="divider" style={{ marginBottom: 14 }} />
|
|
844
|
+
|
|
845
|
+
<div className="flex items-center justify-between gap-4">
|
|
846
|
+
<div className="min-w-0 flex items-center gap-3">
|
|
847
|
+
<div className="text-[12px] tracking-[0.24em] font-semibold whitespace-nowrap" style={{ color: "var(--muted)" }}>
|
|
848
|
+
{token ? "REMOTE FOLLOW" : "LOCAL TERMINAL"}
|
|
849
|
+
</div>
|
|
850
|
+
|
|
851
|
+
<span className="text-[12px] tracking-[0.24em] font-semibold" style={{ color: "var(--muted)" }}>•</span>
|
|
852
|
+
|
|
853
|
+
<h1 className="text-[14px] tracking-[0.06em] font-semibold truncate">
|
|
854
|
+
{workflowName || "WORKFLOW"}
|
|
855
|
+
</h1>
|
|
856
|
+
|
|
857
|
+
<span className="text-[12px] tracking-[0.24em] font-semibold" style={{ color: "var(--muted)" }}>•</span>
|
|
858
|
+
|
|
859
|
+
<StatusBadge status={status} />
|
|
860
|
+
</div>
|
|
861
|
+
|
|
862
|
+
<div className="flex items-center gap-2">
|
|
863
|
+
<Toggle
|
|
864
|
+
onClick={() => setSortNewest((p) => !p)}
|
|
865
|
+
label=""
|
|
866
|
+
title={sortNewest ? "Newest first" : "Oldest first"}
|
|
867
|
+
>
|
|
868
|
+
<span className={`transition-transform duration-100 ${!sortNewest ? "rotate-180" : ""}`}>
|
|
869
|
+
<Icon name="sort" />
|
|
870
|
+
</span>
|
|
871
|
+
</Toggle>
|
|
872
|
+
<Toggle onClick={() => setTheme((p) => (p === "dark" ? "light" : "dark"))} label="" title="Toggle theme">
|
|
873
|
+
{theme === "dark" ? <Icon name="sun" /> : <Icon name="moon" />}
|
|
874
|
+
</Toggle>
|
|
482
875
|
</div>
|
|
483
|
-
<div className="w-1 h-1 rounded-full bg-zinc-300 dark:bg-zinc-800"></div>
|
|
484
|
-
<StatusBadge status={status} />
|
|
485
876
|
</div>
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
<button
|
|
489
|
-
onClick={toggleSort}
|
|
490
|
-
className="p-2.5 rounded-xl bg-gray-200 dark:bg-zinc-900 text-gray-600 dark:text-zinc-400 hover:text-blue-500 transition-all border border-transparent hover:border-gray-300 dark:hover:border-zinc-700"
|
|
491
|
-
title={sortNewest ? "Sort: Newest First" : "Sort: Oldest First"}
|
|
492
|
-
>
|
|
493
|
-
<svg xmlns="http://www.w3.org/2000/svg" className={`h-5 w-5 transition-transform ${!sortNewest ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
494
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" />
|
|
495
|
-
</svg>
|
|
496
|
-
</button>
|
|
497
|
-
<button
|
|
498
|
-
onClick={toggleTheme}
|
|
499
|
-
className="p-2.5 rounded-xl bg-gray-200 dark:bg-zinc-900 text-gray-600 dark:text-zinc-400 hover:text-blue-500 transition-all border border-transparent hover:border-gray-300 dark:hover:border-zinc-700"
|
|
500
|
-
title="Toggle Theme"
|
|
501
|
-
>
|
|
502
|
-
{theme === 'dark' ? <SunIcon /> : <MoonIcon />}
|
|
503
|
-
</button>
|
|
877
|
+
|
|
878
|
+
<div className="divider" style={{ marginTop: 14 }} />
|
|
504
879
|
</div>
|
|
505
880
|
</header>
|
|
506
881
|
|
|
507
|
-
{/* Pending
|
|
882
|
+
{/* Pending interaction always left */}
|
|
508
883
|
{pendingInteraction && (
|
|
509
|
-
<
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
884
|
+
<div className="io-row io-left mb-8">
|
|
885
|
+
<div className="io-card">
|
|
886
|
+
<InteractionForm
|
|
887
|
+
interaction={pendingInteraction}
|
|
888
|
+
onSubmit={handleSubmit}
|
|
889
|
+
disabled={status !== "connected"}
|
|
890
|
+
/>
|
|
891
|
+
</div>
|
|
892
|
+
</div>
|
|
514
893
|
)}
|
|
515
894
|
|
|
516
|
-
{/*
|
|
517
|
-
{
|
|
518
|
-
<div className="
|
|
519
|
-
<div className="
|
|
520
|
-
|
|
895
|
+
{/* Error stays right */}
|
|
896
|
+
{error && (
|
|
897
|
+
<div className="io-row io-right mb-8">
|
|
898
|
+
<div className="io-card">
|
|
899
|
+
<div className="hairline rounded-2xl px-5 py-4 io-out meta-center">
|
|
900
|
+
<div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
901
|
+
ERROR
|
|
902
|
+
</div>
|
|
903
|
+
<div className="mt-2 text-[13px] leading-relaxed markdown-body">{String(error)}</div>
|
|
904
|
+
</div>
|
|
521
905
|
</div>
|
|
522
906
|
</div>
|
|
523
907
|
)}
|
|
524
908
|
|
|
525
|
-
|
|
526
|
-
<div className="flex-1 space-y-12">
|
|
909
|
+
<main className="space-y-10">
|
|
527
910
|
{visibleEvents.length === 0 && (
|
|
528
|
-
<div className="
|
|
529
|
-
|
|
911
|
+
<div className="center-wrap py-16 text-center">
|
|
912
|
+
<div className="text-[11px] tracking-[0.28em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
913
|
+
WAITING FOR EVENTS<span className="blink">…</span>
|
|
914
|
+
</div>
|
|
530
915
|
</div>
|
|
531
916
|
)}
|
|
532
917
|
|
|
533
|
-
{visibleEvents.map(
|
|
534
|
-
|
|
535
|
-
if (item.event.startsWith('WORKFLOW_')) {
|
|
536
|
-
const colorMap = {
|
|
537
|
-
'WORKFLOW_STARTED': 'text-green-500 dark:text-green-400',
|
|
538
|
-
'WORKFLOW_COMPLETED': 'text-blue-510 dark:text-blue-400',
|
|
539
|
-
'WORKFLOW_FAILED': 'text-red-500 dark:text-red-400',
|
|
540
|
-
'WORKFLOW_RESET': 'text-yellow-500 dark:text-yellow-400'
|
|
541
|
-
};
|
|
542
|
-
return (
|
|
543
|
-
<div key={idx} className="flex flex-col items-center py-4">
|
|
544
|
-
<div className="flex items-center space-x-4 text-[10px] uppercase tracking-[0.25em] font-black text-zinc-300 dark:text-zinc-800">
|
|
545
|
-
<div className="h-px w-10 bg-current opacity-20"></div>
|
|
546
|
-
<span className={`${colorMap[item.event] || 'text-zinc-500'} dark:opacity-80`}>
|
|
547
|
-
{item.event.replace('WORKFLOW_', '')}
|
|
548
|
-
</span>
|
|
549
|
-
<span className="text-zinc-400 dark:text-zinc-700 font-medium tracking-normal">{formatTime(item.timestamp)}</span>
|
|
550
|
-
<div className="h-px w-10 bg-current opacity-20"></div>
|
|
551
|
-
</div>
|
|
552
|
-
{item.error && <div className="mt-2 text-red-500 text-xs font-mono max-w-md text-center">{item.error}</div>}
|
|
553
|
-
</div>
|
|
554
|
-
);
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
// 2. Agent Started
|
|
558
|
-
if (item.event === 'AGENT_STARTED') {
|
|
559
|
-
return (
|
|
560
|
-
<div key={idx} className="flex justify-start">
|
|
561
|
-
<div className="text-[10px] text-zinc-400 dark:text-zinc-600 uppercase tracking-widest font-bold flex items-center space-x-2">
|
|
562
|
-
<span className="w-1.5 h-1.5 rounded-full bg-blue-500/50 animate-pulse"></span>
|
|
563
|
-
<span>Agent <span className="text-zinc-800 dark:text-zinc-300">{item.agent}</span> started</span>
|
|
564
|
-
<span>•</span>
|
|
565
|
-
<span>{formatTime(item.timestamp)}</span>
|
|
566
|
-
</div>
|
|
567
|
-
</div>
|
|
568
|
-
);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// 3. Agent Failed
|
|
572
|
-
if (item.event === 'AGENT_FAILED') {
|
|
573
|
-
return (
|
|
574
|
-
<div key={idx} className="flex justify-center">
|
|
575
|
-
<div className="bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-900/50 rounded-2xl px-6 py-4 text-red-600 dark:text-red-400 text-xs font-mono w-full max-w-2xl shadow-sm">
|
|
576
|
-
<div className="font-black mb-2 uppercase tracking-tight">AGENT FAILED: {item.agent}</div>
|
|
577
|
-
<div className="leading-relaxed opacity-80">{item.error}</div>
|
|
578
|
-
</div>
|
|
579
|
-
</div>
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// 4. Interaction / Prompt Requested
|
|
584
|
-
if (item.event === 'INTERACTION_REQUESTED' || item.event === 'PROMPT_REQUESTED') {
|
|
585
|
-
return (
|
|
586
|
-
<div key={idx} className="flex justify-center animate-in fade-in slide-in-from-bottom-2">
|
|
587
|
-
<div className="bg-zinc-100 dark:bg-zinc-900/50 border border-zinc-200 dark:border-zinc-800 border-dashed rounded-2xl px-8 py-6 text-center max-w-md w-full">
|
|
588
|
-
<div className="text-[10px] text-zinc-400 dark:text-zinc-600 uppercase font-black tracking-[0.2em] mb-2 flex items-center justify-center gap-2">
|
|
589
|
-
<span className="w-1 h-1 rounded-full bg-yellow-500"></span>
|
|
590
|
-
Intervention Required
|
|
591
|
-
</div>
|
|
592
|
-
<div className="text-xs text-zinc-600 dark:text-zinc-400 italic font-medium leading-relaxed">
|
|
593
|
-
{item.question ? `"${item.question}"` : `Waiting for response to "${item.slug}"...`}
|
|
594
|
-
</div>
|
|
595
|
-
</div>
|
|
596
|
-
</div>
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// 5. Prompt Answered
|
|
601
|
-
if (item.event === 'PROMPT_ANSWERED' || item.event === 'INTERACTION_SUBMITTED') {
|
|
602
|
-
const isManual = item.source === 'remote';
|
|
603
|
-
return (
|
|
604
|
-
<div key={idx} className="flex justify-center">
|
|
605
|
-
<div className="bg-green-50/50 dark:bg-green-950/10 border border-green-200/50 dark:border-green-900/30 rounded-2xl px-6 py-3 text-center max-w-md w-full">
|
|
606
|
-
<div className="text-[9px] text-green-600 dark:text-green-500 uppercase font-black tracking-widest mb-1">
|
|
607
|
-
{isManual ? 'Resolved via Browser' : 'User Answered'}
|
|
608
|
-
</div>
|
|
609
|
-
<div className="text-xs text-green-800 dark:text-green-300 italic font-bold">
|
|
610
|
-
"{item.answer}"
|
|
611
|
-
</div>
|
|
612
|
-
</div>
|
|
613
|
-
</div>
|
|
614
|
-
);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
// 6. Agent Completed / Interaction Resolved
|
|
618
|
-
if (item.event === 'AGENT_COMPLETED' || item.event === 'INTERACTION_RESOLVED') {
|
|
619
|
-
return (
|
|
620
|
-
<div key={idx} className="flex flex-col space-y-6">
|
|
621
|
-
{/* Header Line */}
|
|
622
|
-
<div className="flex items-center justify-center space-x-3 text-[10px] text-zinc-300 dark:text-zinc-800 uppercase tracking-widest font-black">
|
|
623
|
-
<div className="h-px flex-1 bg-current opacity-20"></div>
|
|
624
|
-
<span className="text-zinc-500 dark:text-zinc-400">{item.agent || item.slug}</span>
|
|
625
|
-
<span className="text-zinc-400 dark:text-zinc-700 font-medium">DONE • {formatTime(item.timestamp)}</span>
|
|
626
|
-
<div className="h-px flex-1 bg-current opacity-20"></div>
|
|
627
|
-
</div>
|
|
628
|
-
|
|
629
|
-
{/* Output (Response) - ON TOP */}
|
|
630
|
-
{(item.output || item.result) && (
|
|
631
|
-
<JsonView
|
|
632
|
-
data={item.output || item.result}
|
|
633
|
-
label="Output / Response"
|
|
634
|
-
onTop={true}
|
|
635
|
-
/>
|
|
636
|
-
)}
|
|
637
|
-
|
|
638
|
-
{/* Prompt (Input) - ON BOTTOM */}
|
|
639
|
-
{item.prompt && (
|
|
640
|
-
<div className="flex justify-start w-full group">
|
|
641
|
-
<div className="max-w-[85%] bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl rounded-tl-none shadow-sm p-8 transition-all hover:border-zinc-300 dark:hover:border-zinc-700 relative">
|
|
642
|
-
<div className="flex justify-between items-center mb-6">
|
|
643
|
-
<div className="text-[9px] font-black text-zinc-300 dark:text-zinc-700 uppercase tracking-[0.3em]">Prompt / Input</div>
|
|
644
|
-
<div className="absolute top-6 right-6 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
645
|
-
<CopyButton text={item.prompt} className="text-gray-400 hover:text-gray-600 dark:text-zinc-600 dark:hover:text-zinc-400" />
|
|
646
|
-
</div>
|
|
647
|
-
</div>
|
|
648
|
-
<div className="markdown-body text-gray-800 dark:text-zinc-300 text-sm overflow-x-auto leading-relaxed">
|
|
649
|
-
{item.prompt}
|
|
650
|
-
</div>
|
|
651
|
-
</div>
|
|
652
|
-
</div>
|
|
653
|
-
)}
|
|
654
|
-
</div>
|
|
655
|
-
);
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
// 7. CATCH-ALL
|
|
659
|
-
return (
|
|
660
|
-
<div key={idx} className="flex justify-center px-4">
|
|
661
|
-
<JsonView
|
|
662
|
-
data={JSON.parse(JSON.stringify(item, (key, value) => {
|
|
663
|
-
if (key === 'event' || key === 'timestamp') return undefined;
|
|
664
|
-
return value;
|
|
665
|
-
}))}
|
|
666
|
-
label={item.event}
|
|
667
|
-
timestamp={formatTime(item.timestamp)}
|
|
668
|
-
/>
|
|
669
|
-
</div>
|
|
670
|
-
);
|
|
671
|
-
})}
|
|
672
|
-
</div>
|
|
918
|
+
{visibleEvents.map(renderEvent)}
|
|
919
|
+
</main>
|
|
673
920
|
|
|
674
|
-
<footer className="mt-
|
|
675
|
-
|
|
921
|
+
<footer className="mt-16 pt-8 divider">
|
|
922
|
+
<div className="center-wrap text-center">
|
|
923
|
+
<div className="py-6 text-[11px] tracking-[0.28em] font-semibold" style={{ color: "var(--muted)" }}>
|
|
924
|
+
SUPAMACHINE • TERMINAL v1.4
|
|
925
|
+
</div>
|
|
926
|
+
</div>
|
|
676
927
|
</footer>
|
|
677
928
|
</div>
|
|
678
929
|
</div>
|
|
@@ -680,7 +931,7 @@
|
|
|
680
931
|
);
|
|
681
932
|
}
|
|
682
933
|
|
|
683
|
-
const root = ReactDOM.createRoot(document.getElementById(
|
|
934
|
+
const root = ReactDOM.createRoot(document.getElementById("root"));
|
|
684
935
|
root.render(<App />);
|
|
685
936
|
</script>
|
|
686
937
|
</body>
|