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.
@@ -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
- .markdown-body {
19
- white-space: pre-wrap;
20
- font-family: monospace;
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
- /* Scrollbar styles for dark mode */
24
- .dark ::-webkit-scrollbar {
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
- .dark ::-webkit-scrollbar-track {
30
- background: #000000;
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
- .dark ::-webkit-scrollbar-thumb {
34
- background: #27272a;
35
- border-radius: 5px;
80
+ .hairline {
81
+ border: 1px solid var(--hairline);
36
82
  }
37
83
 
38
- .dark ::-webkit-scrollbar-thumb:hover {
39
- background: #3f3f46;
84
+ .hairline-strong {
85
+ border: 1px solid var(--hairline-strong);
40
86
  }
41
87
 
42
- @keyframes pulse {
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: .7;
118
+ opacity: 0.4;
51
119
  }
52
120
  }
53
121
 
54
- .animate-pulse-slow {
55
- animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
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
- // Placeholders replaced by server
65
- window.SESSION_TOKEN = '{{SESSION_TOKEN}}';
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 { useState, useEffect, useRef } = React;
71
-
72
- // Icons
73
- const SunIcon = () => (
74
- <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
75
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
76
- </svg>
77
- );
78
-
79
- const MoonIcon = () => (
80
- <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
81
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" />
82
- </svg>
83
- );
84
-
85
- const CopyIcon = () => (
86
- <svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
87
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
88
- </svg>
89
- );
90
-
91
- const CheckIcon = () => (
92
- <svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
93
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
94
- </svg>
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 colors = {
99
- connected: 'bg-green-500',
100
- disconnected: 'bg-red-500',
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 ${colors[status] || colors.disconnected}`}></div>
111
- <span className="text-[10px] uppercase tracking-wider text-zinc-500 font-bold">{labels[status] || status}</span>
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
- const handleCopy = () => {
120
- const content = typeof text === 'object' ? JSON.stringify(text, null, 2) : text;
121
- navigator.clipboard.writeText(content);
122
- setCopied(true);
123
- setTimeout(() => setCopied(false), 2000);
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 space-x-1 text-[9px] uppercase tracking-wider transition-colors hover:text-blue-500 focus:outline-none ${className}`}
130
- title="Copy to clipboard"
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 ? <CheckIcon /> : <CopyIcon />}
133
- <span>{copied ? 'Copied' : 'Copy'}</span>
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 JsonView({ data, label, onTop = false, timestamp }) {
139
- const [viewMode, setViewMode] = useState('clean'); // 'clean' | 'raw'
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
- const isObject = typeof data === 'object' && data !== null;
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 lineBreak = '';
144
-
145
- // "clean" view loops through ALL top-level keys.
146
- let cleanParts = null;
147
- if (isObject) {
148
- const keys = Object.keys(data);
149
- if (keys.length > 0) {
150
- cleanParts = keys.map((k) => {
151
- const val = data[k];
152
- const renderedVal = typeof val === 'string' ? val : JSON.stringify(val, null, 2);
153
- const prettyVal = String(renderedVal).replace(/\\n/g, lineBreak);
154
-
155
- return (
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
- if (onTop) {
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
- <div className="markdown-body text-gray-800 dark:text-zinc-200 text-sm overflow-x-auto leading-relaxed">
194
- {viewMode === 'clean'
195
- ? (cleanParts ?? String(rawContent).replace(/\\n/g, lineBreak))
196
- : rawContentUnescaped
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
- return (
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 === 'clean' ? 'raw' : 'clean')}
215
- className="text-[9px] uppercase tracking-wider font-bold text-zinc-400 hover:text-blue-500 focus:outline-none"
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
- {viewMode === 'clean' ? 'Raw' : 'Clean'}
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} className="text-gray-400 hover:text-gray-600" />
375
+ <CopyButton text={data} />
221
376
  </div>
222
377
  </div>
223
378
 
224
- <div className="text-gray-600 dark:text-zinc-400 whitespace-pre-wrap">
225
- {viewMode === 'clean'
226
- ? (cleanParts ?? String(rawContent).replace(/\\n/g, lineBreak))
227
- : rawContentUnescaped
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
- <div className="bg-yellow-100/50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-700/50 rounded-2xl p-6 mb-8 transition-all hover:border-yellow-300 dark:hover:border-yellow-600/50">
252
- <div className="text-[10px] font-black uppercase tracking-[0.2em] text-yellow-600 dark:text-yellow-500 mb-3 flex items-center gap-2">
253
- <span className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse"></span>
254
- Input Required
255
- </div>
256
- <div className="text-sm text-yellow-900 dark:text-yellow-100/90 mb-5 whitespace-pre-wrap leading-relaxed italic">
257
- {interaction.question || 'Please provide your input.'}
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
- <form onSubmit={handleSubmit} className="flex flex-col gap-4">
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
- className="w-full p-4 bg-white dark:bg-zinc-800 border border-yellow-200 dark:border-zinc-700 rounded-xl text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:outline-none focus:ring-1 focus:ring-yellow-500 transition-all min-h-[100px]"
264
- placeholder="Type your response here..."
265
- disabled={submitting || disabled}
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 justify-end items-center gap-4">
268
- {disabled && <span className="text-[10px] uppercase font-bold text-red-500 tracking-wider">CLI Offline</span>}
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={submitting || disabled || !response.trim()}
272
- className="px-6 py-2.5 bg-yellow-500 hover:bg-yellow-600 text-white font-bold text-xs uppercase tracking-[0.15em] rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-all shadow-sm"
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
- {submitting ? 'Submitting...' : 'Submit Response'}
447
+ <span className="text-[11px] tracking-[0.18em] font-semibold">SUBMIT</span>
275
448
  </button>
276
449
  </div>
277
450
  </form>
278
- </div>
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('connecting');
287
- const [workflowName, setWorkflowName] = useState(window.WORKFLOW_NAME_TEMPLATE || '');
288
- const [theme, setTheme] = useState('dark');
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 = window.SESSION_TOKEN && window.SESSION_TOKEN !== '{{' + 'SESSION_TOKEN' + '}}' ? window.SESSION_TOKEN : null;
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
- // Build API URLs
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 = entry.event === 'INTERACTION_RESOLVED' ||
311
- entry.event === 'PROMPT_ANSWERED' ||
312
- entry.event === 'INTERACTION_SUBMITTED';
313
- const isRequest = entry.event === 'INTERACTION_REQUESTED' ||
314
- entry.event === 'PROMPT_REQUESTED';
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
- // In remote mode, the API also tells us connectivity
340
- if (token && data.cliConnected !== undefined) {
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('connected');
537
+ setStatus("connected");
370
538
  reconnectAttempts = 0;
371
539
  fetchData();
372
540
  };
373
541
 
374
542
  eventSource.onerror = () => {
375
- setStatus('disconnected');
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 === 'update') {
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 'status':
392
- setStatus(data.cliConnected ? 'connected' : 'disconnected');
555
+ case "status":
556
+ setStatus(data.cliConnected ? "connected" : "disconnected");
393
557
  if (data.workflowName) setWorkflowName(data.workflowName);
394
558
  break;
395
- case 'history': setHistory(data.entries || []); break;
396
- case 'event':
397
- setHistory(prev => {
398
- if (data.event === 'INTERACTION_SUBMITTED' && data.slug) {
399
- const hasDupe = prev.some(e =>
400
- e.event === 'INTERACTION_SUBMITTED' && e.slug === data.slug
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 'cli_connected':
408
- case 'cli_reconnected': setStatus('connected'); break;
409
- case 'cli_disconnected': setStatus('disconnected'); break;
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) { /* Not JSON or update ping */ }
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: 'INTERACTION_SUBMITTED',
596
+ event: "INTERACTION_SUBMITTED",
429
597
  slug,
430
598
  targetKey,
431
- answer: response.substring(0, 200) + (response.length > 200 ? '...' : ''),
432
- source: 'remote',
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: 'POST',
439
- headers: { 'Content-Type': 'application/json' },
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 error = await res.json();
445
- throw new Error(error.error || 'Failed to submit');
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(prev => prev === 'dark' ? 'light' : 'dark');
455
- const toggleSort = () => setSortNewest(prev => !prev);
624
+ const toggleTheme = () => setTheme((p) => (p === "dark" ? "light" : "dark"));
625
+ const toggleSort = () => setSortNewest((p) => !p);
456
626
 
457
- if (loading && !history.length) return (
458
- <div className={theme}>
459
- <div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-black text-zinc-500 uppercase tracking-widest text-[10px] font-black">
460
- Opening Terminal...
461
- </div>
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
- let visibleEvents = sortNewest ? history : [...history].reverse();
466
- const formatTime = (ts) => new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
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 bg-gray-50 dark:bg-black transition-colors duration-500">
471
- <div className="max-w-4xl mx-auto min-h-screen flex flex-col p-6">
472
-
473
- {/* Sticky Header */}
474
- <header className="sticky top-0 z-50 py-6 bg-gray-50/90 dark:bg-black/90 backdrop-blur-md border-b border-gray-200 dark:border-zinc-800 flex items-center justify-between mb-8 transition-all">
475
- <div>
476
- <h1 className="text-xl font-black text-gray-800 dark:text-zinc-100 transition-colors uppercase tracking-tight">
477
- {workflowName || 'Workflow'}
478
- </h1>
479
- <div className="flex items-center gap-3 mt-1.5">
480
- <div className="text-zinc-400 dark:text-zinc-600 text-[10px] font-black uppercase tracking-widest">
481
- {token ? 'Remote Follow' : 'Local Terminal'}
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
- </div>
487
- <div className="flex items-center space-x-3">
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 Interaction at Top */}
882
+ {/* Pending interaction always left */}
508
883
  {pendingInteraction && (
509
- <InteractionForm
510
- interaction={pendingInteraction}
511
- onSubmit={handleSubmit}
512
- disabled={status !== 'connected'}
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
- {/* Disconnected Warning */}
517
- {status === 'disconnected' && !pendingInteraction && (
518
- <div className="bg-red-50 dark:bg-red-900/10 border border-red-200 dark:border-red-900/40 rounded-2xl p-4 mb-8 text-center">
519
- <div className="text-[10px] font-black uppercase tracking-widest text-red-600 dark:text-red-500">
520
- Terminal Connection Lost &bull; Retrying...
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
- {/* Content */}
526
- <div className="flex-1 space-y-12">
909
+ <main className="space-y-10">
527
910
  {visibleEvents.length === 0 && (
528
- <div className="text-center text-zinc-400 dark:text-zinc-700 py-20 uppercase text-[10px] font-bold tracking-[0.3em]">
529
- Waiting for events...
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((item, idx) => {
534
- // 1. Lifecycle Events
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>&bull;</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 &bull; {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-32 mb-12 text-center text-zinc-300 dark:text-zinc-800 text-[10px] font-black uppercase tracking-[0.4em] transition-colors">
675
- SUPAMACHINE &bull; Terminal v1.4
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('root'));
934
+ const root = ReactDOM.createRoot(document.getElementById("root"));
684
935
  root.render(<App />);
685
936
  </script>
686
937
  </body>