agent-state-machine 2.0.15 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/bin/cli.js +1 -1
  2. package/lib/index.js +33 -0
  3. package/lib/remote/client.js +7 -2
  4. package/lib/runtime/agent.js +102 -67
  5. package/lib/runtime/index.js +13 -0
  6. package/lib/runtime/interaction.js +304 -0
  7. package/lib/runtime/prompt.js +39 -12
  8. package/lib/runtime/runtime.js +11 -10
  9. package/package.json +1 -1
  10. package/templates/project-builder/agents/assumptions-clarifier.md +0 -1
  11. package/templates/project-builder/agents/code-reviewer.md +0 -1
  12. package/templates/project-builder/agents/code-writer.md +0 -1
  13. package/templates/project-builder/agents/requirements-clarifier.md +0 -1
  14. package/templates/project-builder/agents/response-interpreter.md +25 -0
  15. package/templates/project-builder/agents/roadmap-generator.md +0 -1
  16. package/templates/project-builder/agents/sanity-checker.md +45 -0
  17. package/templates/project-builder/agents/sanity-runner.js +161 -0
  18. package/templates/project-builder/agents/scope-clarifier.md +0 -1
  19. package/templates/project-builder/agents/security-clarifier.md +0 -1
  20. package/templates/project-builder/agents/security-reviewer.md +0 -1
  21. package/templates/project-builder/agents/task-planner.md +0 -1
  22. package/templates/project-builder/agents/test-planner.md +0 -1
  23. package/templates/project-builder/scripts/interaction-helpers.js +33 -0
  24. package/templates/project-builder/scripts/workflow-helpers.js +2 -47
  25. package/templates/project-builder/workflow.js +214 -54
  26. package/vercel-server/api/session/[token].js +3 -3
  27. package/vercel-server/api/submit/[token].js +5 -3
  28. package/vercel-server/local-server.js +33 -6
  29. package/vercel-server/public/remote/index.html +17 -0
  30. package/vercel-server/ui/index.html +9 -1012
  31. package/vercel-server/ui/package-lock.json +2650 -0
  32. package/vercel-server/ui/package.json +25 -0
  33. package/vercel-server/ui/postcss.config.js +6 -0
  34. package/vercel-server/ui/src/App.jsx +236 -0
  35. package/vercel-server/ui/src/components/ChoiceInteraction.jsx +127 -0
  36. package/vercel-server/ui/src/components/ConfirmInteraction.jsx +51 -0
  37. package/vercel-server/ui/src/components/ContentCard.jsx +161 -0
  38. package/vercel-server/ui/src/components/CopyButton.jsx +27 -0
  39. package/vercel-server/ui/src/components/EventsLog.jsx +82 -0
  40. package/vercel-server/ui/src/components/Footer.jsx +66 -0
  41. package/vercel-server/ui/src/components/Header.jsx +38 -0
  42. package/vercel-server/ui/src/components/InteractionForm.jsx +42 -0
  43. package/vercel-server/ui/src/components/TextInteraction.jsx +72 -0
  44. package/vercel-server/ui/src/index.css +145 -0
  45. package/vercel-server/ui/src/main.jsx +8 -0
  46. package/vercel-server/ui/tailwind.config.js +19 -0
  47. package/vercel-server/ui/vite.config.js +11 -0
@@ -1,1019 +1,16 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
-
4
- <head>
3
+ <head>
5
4
  <meta charset="UTF-8" />
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
- <title>{{WORKFLOW_NAME}} - Remote Follow</title>
8
-
9
- <script src="https://cdn.tailwindcss.com"></script>
10
- <script>
11
- tailwind.config = { darkMode: "class" };
12
- </script>
13
-
14
- <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script>
15
- <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script>
16
- <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
17
-
18
- <style>
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);
31
- }
32
-
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 {
61
- width: 10px;
62
- height: 10px;
63
- }
64
-
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);
78
- }
79
-
80
- .hairline {
81
- border: 1px solid var(--hairline);
82
- }
83
-
84
- .hairline-strong {
85
- border: 1px solid var(--hairline-strong);
86
- }
87
-
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
- .agent-prompt summary {
111
- list-style: none;
112
- }
113
-
114
- .agent-prompt summary::-webkit-details-marker {
115
- display: none;
116
- }
117
-
118
- @keyframes blink {
119
-
120
- 0%,
121
- 100% {
122
- opacity: 1;
123
- }
124
-
125
- 50% {
126
- opacity: 0.4;
127
- }
128
- }
129
-
130
- .blink {
131
- animation: blink 1.1s steps(2, end) infinite;
132
- }
133
-
134
- .glow-dot {
135
- background: var(--fg);
136
- box-shadow: 0 0 0 2px var(--chip), 0 0 10px var(--fg);
137
- }
138
-
139
- button,
140
- textarea {
141
- -webkit-tap-highlight-color: transparent;
142
- }
143
-
144
- button:focus-visible,
145
- textarea:focus-visible {
146
- outline: 2px solid var(--focus);
147
- outline-offset: 2px;
148
- }
149
-
150
- /* === ONE COLUMN, "INPUT" LEFT / "OUTPUT" RIGHT, ALL LTR === */
151
- .edge-wrap {
152
- width: 100%;
153
- padding-left: 12px;
154
- padding-right: 12px;
155
- }
156
-
157
- @media (min-width: 640px) {
158
- .edge-wrap {
159
- padding-left: 16px;
160
- padding-right: 16px;
161
- }
162
- }
163
-
164
- .io-row {
165
- display: flex;
166
- width: 100%;
167
- }
168
-
169
- .io-left {
170
- justify-content: flex-start;
171
- }
172
-
173
- .io-right {
174
- justify-content: flex-end;
175
- }
176
-
177
- .io-center {
178
- justify-content: center;
179
- }
180
-
181
- .io-card {
182
- width: min(880px, 100%);
183
- }
184
-
185
- .io-in {
186
- text-align: left;
187
- direction: ltr;
188
- }
189
-
190
- .io-out {
191
- text-align: right;
192
- direction: ltr;
193
- }
194
-
195
- .rtl-safe {
196
- direction: ltr;
197
- }
198
-
199
- /* centered chrome (header/footer/empty states) */
200
- .center-wrap {
201
- width: min(880px, 100%);
202
- margin-left: auto;
203
- margin-right: auto;
204
- }
205
-
206
- /* NEW: center the “meta rows” inside their cards */
207
- .meta-center {
208
- text-align: center;
209
- }
210
- </style>
211
- </head>
212
-
213
- <body>
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
6
+ <title>{{WORKFLOW_NAME}}</title>
7
+ </head>
8
+ <body>
214
9
  <div id="root"></div>
215
-
216
10
  <script>
217
- window.SESSION_TOKEN = "{{SESSION_TOKEN}}";
218
- window.WORKFLOW_NAME_TEMPLATE = "{{WORKFLOW_NAME}}";
219
- </script>
220
-
221
- <script type="text/babel">
222
- const { useEffect, useMemo, useRef, useState } = React;
223
-
224
- const Icon = ({ name }) => {
225
- const common = "w-4 h-4";
226
- if (name === "sun") {
227
- return (
228
- <svg className={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
229
- <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" />
230
- <circle cx="12" cy="12" r="4" />
231
- </svg>
232
- );
233
- }
234
- if (name === "moon") {
235
- return (
236
- <svg className={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
237
- <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" />
238
- </svg>
239
- );
240
- }
241
- if (name === "copy") {
242
- return (
243
- <svg className={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
244
- <rect x="9" y="9" width="13" height="13" rx="2" />
245
- <path strokeLinecap="round" d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
246
- </svg>
247
- );
248
- }
249
- if (name === "check") {
250
- return (
251
- <svg className={common} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
252
- <path strokeLinecap="round" strokeLinejoin="round" d="M20 6 9 17l-5-5" />
253
- </svg>
254
- );
255
- }
256
- return null;
257
- };
258
-
259
- function StatusBadge({ status }) {
260
- const labels = { connected: "Live", disconnected: "Offline", connecting: "Connecting..." };
261
- const dotClass =
262
- status === "connecting" ? "blink" : status === "connected" ? "blink glow-dot" : "";
263
- return (
264
- <div className="flex items-center gap-2">
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>
269
- </div>
270
- );
271
- }
272
-
273
- function CopyButton({ text, className = "" }) {
274
- const [copied, setCopied] = useState(false);
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
- }
291
- };
292
- return (
293
- <button
294
- onClick={handleCopy}
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" }}
299
- >
300
- {copied ? <Icon name="check" /> : <Icon name="copy" />}
301
- <span className="text-[11px] tracking-[0.14em] font-semibold">{copied ? "COPIED" : "COPY"}</span>
302
- </button>
303
- );
304
- }
305
-
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
- }
320
-
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;
324
- const rawContent = isObject ? JSON.stringify(data, null, 2) : String(data);
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()}
338
- </div>
339
- <div className="markdown-body text-[13px] leading-relaxed">{String(renderedVal)}</div>
340
- </div>
341
- );
342
- });
343
- }, [data, isObject]);
344
-
345
- const ioClass = align === "right" ? "io-out" : "io-in";
346
-
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}
353
- </div>
354
- {timestamp && (
355
- <span className="text-[11px] tracking-[0.12em]" style={{ color: "var(--muted)" }}>
356
- {timestamp}
357
- </span>
358
- )}
359
- </div>
360
-
361
- <div className="flex items-center gap-2">
362
- {hasToggle && (
363
- <button
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)" }}
369
- >
370
- <span className="text-[11px] tracking-[0.14em] font-semibold">
371
- {viewMode === "clean" ? "RAW" : "CLEAN"}
372
- </span>
373
- </button>
374
- )}
375
- <CopyButton text={data} />
376
- </div>
377
- </div>
378
-
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>
383
- </div>
384
- </div>
385
- );
386
- }
387
-
388
- function InteractionForm({ interaction, onSubmit, disabled }) {
389
- const [response, setResponse] = useState("");
390
- const [submitting, setSubmitting] = useState(false);
391
- const textareaRef = useRef(null);
392
-
393
- useEffect(() => {
394
- setResponse(interaction.question || "");
395
- }, [interaction.slug, interaction.question]);
396
-
397
- useEffect(() => {
398
- const el = textareaRef.current;
399
- if (!el) return;
400
- el.style.height = "auto";
401
- el.style.height = `${el.scrollHeight}px`;
402
- }, [response]);
403
-
404
- const handleSubmit = async (e) => {
405
- e.preventDefault();
406
- if (!response.trim() || submitting) return;
407
- setSubmitting(true);
408
- try {
409
- await onSubmit(interaction.slug, interaction.targetKey, response.trim());
410
- setResponse("");
411
- } finally {
412
- setSubmitting(false);
413
- }
414
- };
415
-
416
- return (
417
- <section className="hairline rounded-2xl overflow-hidden io-in">
418
- <div className="rtl-safe px-6 py-5 divider flex items-center justify-between">
419
- <div className="min-w-0">
420
- <div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
421
- INPUT REQUIRED
422
- </div>
423
- <div className="mt-2 text-[12px] tracking-[0.16em] uppercase" style={{ color: "var(--muted)" }}>
424
- Safe to delete all text and type your answer.
425
- </div>
426
- </div>
427
- <div className="flex items-center gap-2">
428
- {disabled && <span className="kbd" title="CLI offline">OFFLINE</span>}
429
- </div>
430
- </div>
431
-
432
- <form onSubmit={handleSubmit} className="px-6 py-5">
433
- <textarea
434
- ref={textareaRef}
435
- value={response}
436
- onChange={(e) => setResponse(e.target.value)}
437
- disabled={disabled || submitting}
438
- className="w-full rounded-2xl hairline p-4 text-[13px] leading-relaxed min-h-[120px]"
439
- style={{
440
- background: "transparent",
441
- color: "var(--fg)",
442
- borderColor: "var(--hairline)",
443
- outline: "none",
444
- resize: "none",
445
- direction: "ltr",
446
- textAlign: "left",
447
- }}
448
- />
449
- <div className="mt-4 flex items-center justify-between gap-3 rtl-safe">
450
- <div className="text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
451
- {submitting ? "SENDING…" : " "}
452
- </div>
453
- <button
454
- type="submit"
455
- disabled={disabled || submitting || !response.trim()}
456
- className="px-4 py-2 rounded-xl hairline hover:hairline-strong transition disabled:opacity-40 disabled:cursor-not-allowed"
457
- style={{ background: "transparent", color: "var(--fg)" }}
458
- >
459
- <span className="text-[11px] tracking-[0.18em] font-semibold">SUBMIT</span>
460
- </button>
461
- </div>
462
- </form>
463
- </section>
464
- );
465
- }
466
-
467
- function App() {
468
- const [history, setHistory] = useState([]);
469
- const [loading, setLoading] = useState(true);
470
- const [error, setError] = useState(null);
471
- const [status, setStatus] = useState("connecting");
472
- const [workflowName, setWorkflowName] = useState(window.WORKFLOW_NAME_TEMPLATE || "");
473
- const [theme, setTheme] = useState(() => {
474
- const saved = localStorage.getItem("rf_theme");
475
- if (saved === "light" || saved === "dark") return saved;
476
- return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
477
- });
478
- const [pendingInteraction, setPendingInteraction] = useState(null);
479
-
480
- const token =
481
- window.SESSION_TOKEN && window.SESSION_TOKEN !== "{{" + "SESSION_TOKEN" + "}}" ? window.SESSION_TOKEN : null;
482
-
483
- const historyUrl = token ? `/api/history/${token}` : "/api/history";
484
- const eventsUrl = token ? `/api/events/${token}` : "/api/events";
485
- const submitUrl = token ? `/api/submit/${token}` : "/api/submit";
486
-
487
- useEffect(() => localStorage.setItem("rf_theme", theme), [theme]);
488
-
489
- // Helper to check if workflow is currently running based on history
490
- const isWorkflowRunning = (entries) => {
491
- // Find the most recent workflow lifecycle event (history is newest-first)
492
- for (const entry of entries) {
493
- if (entry.event === "WORKFLOW_STARTED") return true;
494
- if (entry.event === "WORKFLOW_STOPPED" ||
495
- entry.event === "WORKFLOW_COMPLETED" ||
496
- entry.event === "WORKFLOW_FAILED") return false;
497
- }
498
- return false; // No lifecycle events found
499
- };
500
-
501
- useEffect(() => {
502
- if (history.length === 0) { setPendingInteraction(null); return; }
503
-
504
- const resolvedSlugs = new Set();
505
- let pending = null;
506
- const workflowRunning = isWorkflowRunning(history);
507
-
508
- for (const entry of history) {
509
- const isResolution =
510
- entry.event === "INTERACTION_RESOLVED" ||
511
- entry.event === "PROMPT_ANSWERED" ||
512
- entry.event === "INTERACTION_SUBMITTED";
513
- const isRequest = entry.event === "INTERACTION_REQUESTED" || entry.event === "PROMPT_REQUESTED";
514
-
515
- if (isResolution && entry.slug) resolvedSlugs.add(entry.slug);
516
-
517
- if (isRequest && entry.slug && !resolvedSlugs.has(entry.slug) && !pending) {
518
- pending = {
519
- slug: entry.slug,
520
- targetKey: entry.targetKey || `_interaction_${entry.slug}`,
521
- question: entry.question,
522
- };
523
- }
524
- }
525
-
526
- // Only show pending interaction if workflow is running
527
- setPendingInteraction(workflowRunning ? pending : null);
528
- }, [history]);
529
-
530
- const fetchData = async () => {
531
- try {
532
- const res = await fetch(historyUrl);
533
- const data = await res.json();
534
- if (data.entries) setHistory(data.entries);
535
- if (data.workflowName) setWorkflowName(data.workflowName);
536
-
537
- // Check if workflow is currently running based on most recent lifecycle event
538
- const workflowRunning = isWorkflowRunning(data.entries || []);
539
-
540
- if (workflowRunning) {
541
- setStatus("connected");
542
- } else if (token && data.cliConnected !== undefined) {
543
- setStatus(data.cliConnected ? "connected" : "disconnected");
544
- } else if (!token) {
545
- setStatus("connected");
546
- } else {
547
- setStatus("disconnected");
548
- }
549
-
550
- setLoading(false);
551
- return true;
552
- } catch (err) {
553
- setError(err.message);
554
- setLoading(false);
555
- return false;
556
- }
557
- };
558
-
559
- useEffect(() => {
560
- fetchData();
561
-
562
- let eventSource = null;
563
- let reconnectTimeout = null;
564
- let pollInterval = null;
565
- let reconnectAttempts = 0;
566
-
567
- const connect = () => {
568
- if (eventSource) eventSource.close();
569
- eventSource = new EventSource(eventsUrl);
570
-
571
- eventSource.onopen = () => {
572
- setStatus("connected");
573
- reconnectAttempts = 0;
574
- fetchData();
575
- };
576
-
577
- eventSource.onerror = () => {
578
- setStatus("disconnected");
579
- eventSource.close();
580
- const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000);
581
- reconnectAttempts++;
582
- reconnectTimeout = setTimeout(connect, delay);
583
- };
584
-
585
- eventSource.onmessage = (e) => {
586
- try {
587
- if (e.data === "update") { fetchData(); return; }
588
- const data = JSON.parse(e.data);
589
- switch (data.type) {
590
- case "status":
591
- setStatus(data.cliConnected ? "connected" : "disconnected");
592
- if (data.workflowName) setWorkflowName(data.workflowName);
593
- break;
594
- case "history":
595
- setHistory(data.entries || []);
596
- // Update status based on workflow lifecycle
597
- setStatus(isWorkflowRunning(data.entries || []) ? "connected" : "disconnected");
598
- break;
599
- case "event":
600
- setHistory((prev) => {
601
- if (data.event === "INTERACTION_SUBMITTED" && data.slug) {
602
- const hasDupe = prev.some((e) => e.event === "INTERACTION_SUBMITTED" && e.slug === data.slug);
603
- if (hasDupe) return prev;
604
- }
605
- return [data, ...prev];
606
- });
607
- // Update status based on workflow lifecycle events
608
- if (data.event === "WORKFLOW_STARTED") {
609
- setStatus("connected");
610
- } else if (data.event === "WORKFLOW_STOPPED" ||
611
- data.event === "WORKFLOW_COMPLETED" ||
612
- data.event === "WORKFLOW_FAILED") {
613
- setStatus("disconnected");
614
- }
615
- break;
616
- case "cli_connected":
617
- case "cli_reconnected":
618
- setStatus("connected");
619
- break;
620
- case "cli_disconnected":
621
- setStatus("disconnected");
622
- break;
623
- }
624
- } catch (err) { }
625
- };
626
- };
627
-
628
- connect();
629
- pollInterval = setInterval(fetchData, 10000);
630
-
631
- return () => {
632
- if (eventSource) eventSource.close();
633
- if (reconnectTimeout) clearTimeout(reconnectTimeout);
634
- if (pollInterval) clearInterval(pollInterval);
635
- };
636
- }, []);
637
-
638
- const handleSubmit = async (slug, targetKey, response) => {
639
- const optimisticEvent = {
640
- timestamp: new Date().toISOString(),
641
- event: "INTERACTION_SUBMITTED",
642
- slug,
643
- targetKey,
644
- answer: response.substring(0, 200) + (response.length > 200 ? "..." : ""),
645
- source: "remote",
646
- };
647
- setHistory((prev) => [optimisticEvent, ...prev]);
648
-
649
- try {
650
- const res = await fetch(submitUrl, {
651
- method: "POST",
652
- headers: { "Content-Type": "application/json" },
653
- body: JSON.stringify({ slug, targetKey, response }),
654
- });
655
-
656
- if (!res.ok) {
657
- setHistory((prev) => prev.filter((e) => e !== optimisticEvent));
658
- const err = await res.json();
659
- throw new Error(err.error || "Failed to submit");
660
- }
661
-
662
- setTimeout(fetchData, 1000);
663
- } catch (err) {
664
- setHistory((prev) => prev.filter((e) => e !== optimisticEvent));
665
- alert(err.message);
666
- }
667
- };
668
-
669
- const toggleTheme = () => setTheme((p) => (p === "dark" ? "light" : "dark"));
670
-
671
- const formatTime = (ts) =>
672
- new Date(ts).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
673
-
674
- const visibleEvents = [...history].reverse();
675
-
676
- const wrapIO = (side, children, key) => (
677
- <div key={key} className={`io-row ${side === "right" ? "io-right" : side === "center" ? "io-center" : "io-left"}`}>
678
- <div className="io-card">{children}</div>
679
- </div>
680
- );
681
-
682
- const renderPromptInputCard = (prompt, key) =>
683
- wrapIO(
684
- "left",
685
- <section className="hairline rounded-2xl rounded-tl-none overflow-hidden io-in">
686
- <div className="rtl-safe px-5 py-4 divider flex items-center justify-between">
687
- <div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
688
- PROMPT / INPUT
689
- </div>
690
- <CopyButton text={prompt} />
691
- </div>
692
- <div className="px-5 py-4">
693
- <div className="markdown-body text-[13px] leading-relaxed overflow-x-auto">{prompt}</div>
694
- </div>
695
- </section>,
696
- key
697
- );
698
-
699
- const renderEvent = (item, idx) => {
700
- const time = formatTime(item.timestamp);
701
-
702
- if (item.event && item.event.startsWith("WORKFLOW_")) {
703
- return wrapIO(
704
- "center",
705
- <section className="text-center">
706
- <div className="py-4">
707
- <div className="text-[11px] tracking-[0.24em] font-semibold" style={{ color: "var(--muted)" }}>
708
- {item.event.replace("WORKFLOW_", "")} • {time}
709
- </div>
710
- {item.error && <div className="mt-3 text-[13px] leading-relaxed markdown-body">{item.error}</div>}
711
- </div>
712
- </section>,
713
- idx
714
- );
715
- }
716
-
717
- // CENTERED inside card
718
- if (item.event === "AGENT_STARTED") {
719
- if (item.prompt) {
720
- return wrapIO(
721
- "left",
722
- <section className="hairline rounded-2xl rounded-tl-none overflow-hidden io-in">
723
- <details className="agent-prompt">
724
- <summary className="rtl-safe px-5 py-4 divider cursor-pointer">
725
- <div className="flex items-center justify-between gap-4">
726
- <div>
727
- <div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
728
- AGENT STARTED
729
- </div>
730
- <div className="mt-1 text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
731
- {(item.agent || "").toString()} • {time}
732
- </div>
733
- </div>
734
- <CopyButton text={item.prompt} />
735
- </div>
736
- <div className="mt-3 text-[11px] tracking-[0.18em] font-semibold uppercase" style={{ color: "var(--muted)" }}>
737
- Show Prompt ▼
738
- </div>
739
- </summary>
740
- <div className="px-5 py-4">
741
- <div className="markdown-body text-[13px] leading-relaxed overflow-x-auto">{item.prompt}</div>
742
- </div>
743
- </details>
744
- </section>,
745
- idx
746
- );
747
- }
748
-
749
- return wrapIO(
750
- "left",
751
- <section className="hairline rounded-2xl rounded-tl-none px-6 py-5 io-in">
752
- <div className="rtl-safe flex items-center justify-between gap-4">
753
- <div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
754
- AGENT STARTED
755
- </div>
756
- <span className="text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
757
- {time}
758
- </span>
759
- </div>
760
- <div className="mt-3 text-[13px] leading-relaxed">
761
- <span className="font-semibold">{item.agent}</span>
762
- </div>
763
- </section>,
764
- idx
765
- );
766
- }
767
-
768
- if (item.event === "AGENT_RESUMED") {
769
- return wrapIO(
770
- "center",
771
- <section className="meta-center">
772
- <div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
773
- AGENT RESUMED
774
- </div>
775
- <div className="mt-2 text-[13px] leading-relaxed">
776
- <span className="font-semibold">{item.agent}</span>
777
- </div>
778
- <div className="mt-1 text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
779
- {time}
780
- </div>
781
- </section>,
782
- idx
783
- );
784
- }
785
-
786
- if (item.event === "AGENT_FAILED") {
787
- return wrapIO(
788
- "center",
789
- <section className="hairline rounded-2xl overflow-hidden">
790
- <div className="rtl-safe px-5 py-4 divider flex items-center justify-between">
791
- <div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
792
- AGENT FAILED
793
- </div>
794
- <span className="text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
795
- {time}
796
- </span>
797
- </div>
798
- <div className="px-5 py-4 meta-center">
799
- <div className="text-[13px] leading-relaxed"><span className="font-semibold">{item.agent}</span></div>
800
- <div className="mt-3 text-[13px] leading-relaxed markdown-body">{item.error}</div>
801
- </div>
802
- </section>,
803
- idx
804
- );
805
- }
806
-
807
- if (item.event === "INTERACTION_REQUESTED" || item.event === "PROMPT_REQUESTED") {
808
- return wrapIO(
809
- "center",
810
- <section className="hairline rounded-2xl px-6 py-5">
811
- <div className="rtl-safe flex items-center justify-between gap-4">
812
- <div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
813
- INTERVENTION REQUIRED
814
- </div>
815
- <span className="text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
816
- {time}
817
- </span>
818
- </div>
819
- <div className="mt-3 text-[13px] leading-relaxed markdown-body">
820
- {item.question ? `"${item.question}"` : `Waiting for response to "${item.slug}"…`}
821
- </div>
822
- </section>,
823
- idx
824
- );
825
- }
826
-
827
- if (item.event === "PROMPT_ANSWERED" || item.event === "INTERACTION_SUBMITTED") {
828
- const isManual = item.source === "remote";
829
- return wrapIO(
830
- "center",
831
- <section className="hairline rounded-2xl px-6 py-5">
832
- <div className="rtl-safe flex items-center justify-between gap-4">
833
- <div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
834
- {isManual ? "RESOLVED VIA BROWSER" : "USER ANSWERED"}
835
- </div>
836
- <span className="text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
837
- {time}
838
- </span>
839
- </div>
840
- <div className="mt-3 text-[13px] leading-relaxed markdown-body">"{item.answer}"</div>
841
- </section>,
842
- idx
843
- );
844
- }
845
-
846
- if (item.event === "AGENT_COMPLETED" || item.event === "INTERACTION_RESOLVED") {
847
- if (item.event === "AGENT_COMPLETED") {
848
- return wrapIO(
849
- "right",
850
- <section className="io-out">
851
- <section className="meta-center mb-4">
852
- <div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
853
- DONE
854
- </div>
855
- <div className="mt-1 text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
856
- {(item.agent || "").toString()} • {time}
857
- </div>
858
- </section>
859
- {(item.output || item.result) && (
860
- <JsonView data={item.output || item.result} label="OUTPUT / RESPONSE" align="right" />
861
- )}
862
- </section>,
863
- idx
864
- );
865
- }
866
-
867
- return wrapIO(
868
- "center",
869
- <section className="meta-center">
870
- <div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
871
- DONE
872
- </div>
873
- <div className="mt-1 text-[11px] tracking-[0.14em]" style={{ color: "var(--muted)" }}>
874
- {(item.slug || "").toString()} • {time}
875
- </div>
876
- </section>,
877
- idx
878
- );
879
- }
880
-
881
- const stripped = JSON.parse(
882
- JSON.stringify(item, (key, value) => {
883
- if (key === "event" || key === "timestamp") return undefined;
884
- return value;
885
- })
886
- );
887
-
888
- if (item.prompt) {
889
- return (
890
- <React.Fragment key={idx}>
891
- {wrapIO(
892
- "center",
893
- <section>
894
- <JsonView
895
- data={stripped}
896
- label={(item.event || "EVENT").toString()}
897
- timestamp={time}
898
- align="left"
899
- />
900
- </section>,
901
- `evt-${idx}`
902
- )}
903
- {renderPromptInputCard(item.prompt, `prompt-${idx}`)}
904
- </React.Fragment>
905
- );
906
- }
907
-
908
- return wrapIO(
909
- "center",
910
- <section>
911
- <JsonView
912
- data={stripped}
913
- label={(item.event || "EVENT").toString()}
914
- timestamp={time}
915
- align="left"
916
- />
917
- </section>,
918
- idx
919
- );
920
- };
921
-
922
- if (loading && !history.length) {
923
- return (
924
- <div className={theme}>
925
- <div className="min-h-screen flex items-center justify-center">
926
- <div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
927
- OPENING TERMINAL<span className="blink">…</span>
928
- </div>
929
- </div>
930
- </div>
931
- );
932
- }
933
-
934
- return (
935
- <div className={theme}>
936
- <div className="min-h-screen" style={{ background: "var(--bg)", color: "var(--fg)" }}>
937
- <div className="edge-wrap">
938
- {/* Header (centered) */}
939
- <header className="sticky top-0 z-50 py-4" style={{ background: "var(--bg)" }}>
940
- <div className="center-wrap">
941
- <div className="divider" style={{ marginBottom: 14 }} />
942
-
943
- <div className="flex items-center justify-between gap-4">
944
- <div className="min-w-0 flex items-center gap-3">
945
- <div className="text-[12px] tracking-[0.24em] font-semibold uppercase whitespace-nowrap" style={{ color: "var(--muted)" }}>
946
- {workflowName || "WORKFLOW"}
947
- </div>
948
-
949
- <StatusBadge status={status} />
950
- </div>
951
-
952
- <div className="flex items-center gap-2">
953
- <Toggle onClick={() => setTheme((p) => (p === "dark" ? "light" : "dark"))} label="" title="Toggle theme">
954
- {theme === "dark" ? <Icon name="sun" /> : <Icon name="moon" />}
955
- </Toggle>
956
- </div>
957
- </div>
958
-
959
- <div className="divider" style={{ marginTop: 14 }} />
960
- </div>
961
- </header>
962
-
963
- {/* Error stays right */}
964
- {error && (
965
- <div className="io-row io-right mb-8">
966
- <div className="io-card">
967
- <div className="hairline rounded-2xl px-5 py-4 io-out meta-center">
968
- <div className="text-[11px] tracking-[0.22em] font-semibold" style={{ color: "var(--muted)" }}>
969
- ERROR
970
- </div>
971
- <div className="mt-2 text-[13px] leading-relaxed markdown-body">{String(error)}</div>
972
- </div>
973
- </div>
974
- </div>
975
- )}
976
-
977
- <main className="space-y-10">
978
- {visibleEvents.length === 0 && (
979
- <div className="center-wrap py-16 text-center">
980
- <div className="text-[11px] tracking-[0.28em] font-semibold" style={{ color: "var(--muted)" }}>
981
- WAITING FOR EVENTS<span className="blink">…</span>
982
- </div>
983
- </div>
984
- )}
985
-
986
- {visibleEvents.map(renderEvent)}
987
- </main>
988
-
989
- {pendingInteraction && (
990
- <div className="io-row io-left mt-10">
991
- <div className="io-card">
992
- <InteractionForm
993
- interaction={pendingInteraction}
994
- onSubmit={handleSubmit}
995
- disabled={status !== "connected"}
996
- />
997
- </div>
998
- </div>
999
- )}
1000
-
1001
- <footer className="mt-16 pt-8 divider">
1002
- <div className="center-wrap text-center">
1003
- <div className="py-6 text-[11px] tracking-[0.28em] font-semibold" style={{ color: "var(--muted)" }}>
1004
- SUPAMACHINE • TERMINAL v1.4
1005
- </div>
1006
- </div>
1007
- </footer>
1008
- </div>
1009
- </div>
1010
- </div>
1011
- );
1012
- }
1013
-
1014
- const root = ReactDOM.createRoot(document.getElementById("root"));
1015
- root.render(<App />);
11
+ window.SESSION_TOKEN = "{{SESSION_TOKEN}}";
12
+ window.WORKFLOW_NAME_TEMPLATE = "{{WORKFLOW_NAME}}";
1016
13
  </script>
1017
- </body>
1018
-
14
+ <script type="module" src="/src/main.jsx"></script>
15
+ </body>
1019
16
  </html>