agent-state-machine 1.4.2 → 2.0.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.
@@ -4,7 +4,7 @@
4
4
  <head>
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <title>Workflow Prompts Viewer</title>
7
+ <title>{{WORKFLOW_NAME}} - Remote Follow</title>
8
8
  <script src="https://cdn.tailwindcss.com"></script>
9
9
  <script>
10
10
  tailwind.config = {
@@ -38,14 +38,36 @@
38
38
  .dark ::-webkit-scrollbar-thumb:hover {
39
39
  background: #3f3f46;
40
40
  }
41
+
42
+ @keyframes pulse {
43
+
44
+ 0%,
45
+ 100% {
46
+ opacity: 1;
47
+ }
48
+
49
+ 50% {
50
+ opacity: .7;
51
+ }
52
+ }
53
+
54
+ .animate-pulse-slow {
55
+ animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
56
+ }
41
57
  </style>
42
58
  </head>
43
59
 
44
60
  <body>
45
61
  <div id="root"></div>
46
62
 
63
+ <script>
64
+ // Placeholders replaced by server
65
+ window.SESSION_TOKEN = '{{SESSION_TOKEN}}';
66
+ window.WORKFLOW_NAME_TEMPLATE = '{{WORKFLOW_NAME}}';
67
+ </script>
68
+
47
69
  <script type="text/babel">
48
- const { useState, useEffect } = React;
70
+ const { useState, useEffect, useRef } = React;
49
71
 
50
72
  // Icons
51
73
  const SunIcon = () => (
@@ -72,6 +94,25 @@
72
94
  </svg>
73
95
  );
74
96
 
97
+ 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
+ };
108
+ return (
109
+ <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>
112
+ </div>
113
+ );
114
+ }
115
+
75
116
  function CopyButton({ text, className }) {
76
117
  const [copied, setCopied] = useState(false);
77
118
 
@@ -101,8 +142,7 @@
101
142
  const rawContent = isObject ? JSON.stringify(data, null, 2) : String(data);
102
143
  const lineBreak = '';
103
144
 
104
- // UPDATED: "clean" view loops through ALL top-level keys.
105
- // Headers render in CAPS, BOLD, and BLUE.
145
+ // "clean" view loops through ALL top-level keys.
106
146
  let cleanParts = null;
107
147
  if (isObject) {
108
148
  const keys = Object.keys(data);
@@ -117,7 +157,7 @@
117
157
  <div className="text-[11px] font-extrabold uppercase tracking-wider text-blue-600 dark:text-blue-400 mb-2">
118
158
  {k}
119
159
  </div>
120
- <div className="whitespace-pre-wrap">
160
+ <div className="whitespace-pre-wrap leading-relaxed">
121
161
  {prettyVal}
122
162
  </div>
123
163
  </div>
@@ -126,9 +166,7 @@
126
166
  }
127
167
  }
128
168
 
129
- // For RAW view we still render as a string
130
- const rawContentUnescaped = rawContent; // keep raw exact
131
-
169
+ const rawContentUnescaped = rawContent;
132
170
  const hasToggle = isObject || rawContent.includes('\\n');
133
171
 
134
172
  if (onTop) {
@@ -193,107 +231,272 @@
193
231
  );
194
232
  }
195
233
 
234
+ function InteractionForm({ interaction, onSubmit, disabled }) {
235
+ const [response, setResponse] = useState('');
236
+ const [submitting, setSubmitting] = useState(false);
237
+
238
+ const handleSubmit = async (e) => {
239
+ e.preventDefault();
240
+ if (!response.trim() || submitting) return;
241
+ setSubmitting(true);
242
+ try {
243
+ await onSubmit(interaction.slug, interaction.targetKey, response.trim());
244
+ setResponse('');
245
+ } finally {
246
+ setSubmitting(false);
247
+ }
248
+ };
249
+
250
+ 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.'}
258
+ </div>
259
+ <form onSubmit={handleSubmit} className="flex flex-col gap-4">
260
+ <textarea
261
+ value={response}
262
+ 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}
266
+ />
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>}
269
+ <button
270
+ 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"
273
+ >
274
+ {submitting ? 'Submitting...' : 'Submit Response'}
275
+ </button>
276
+ </div>
277
+ </form>
278
+ </div>
279
+ );
280
+ }
281
+
196
282
  function App() {
197
283
  const [history, setHistory] = useState([]);
198
284
  const [loading, setLoading] = useState(true);
199
285
  const [error, setError] = useState(null);
200
- const [workflowName, setWorkflowName] = useState('');
286
+ const [status, setStatus] = useState('connecting');
287
+ const [workflowName, setWorkflowName] = useState(window.WORKFLOW_NAME_TEMPLATE || '');
201
288
  const [theme, setTheme] = useState('dark');
202
- const [sortOrder, setSortOrder] = useState('newest'); // 'newest' | 'oldest'
289
+ const [sortNewest, setSortNewest] = useState(true);
290
+ const [pendingInteraction, setPendingInteraction] = useState(null);
291
+
292
+ const token = window.SESSION_TOKEN && window.SESSION_TOKEN !== '{{' + 'SESSION_TOKEN' + '}}' ? window.SESSION_TOKEN : null;
293
+
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';
203
298
 
299
+ // Detect pending interactions
204
300
  useEffect(() => {
205
- const fetchData = () => {
206
- fetch('/api/history')
207
- .then(res => res.json())
208
- .then(data => {
209
- setHistory(data.entries);
210
- setWorkflowName(data.workflowName);
211
- setLoading(false);
212
- })
213
- .catch(err => {
214
- setError(err.message);
215
- setLoading(false);
216
- });
217
- };
301
+ if (history.length === 0) {
302
+ setPendingInteraction(null);
303
+ return;
304
+ }
305
+
306
+ const resolvedSlugs = new Set();
307
+ let pending = null;
308
+
309
+ 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
+ }
218
319
 
219
- // Initial fetch
320
+ if (isRequest && entry.slug && !resolvedSlugs.has(entry.slug) && !pending) {
321
+ pending = {
322
+ slug: entry.slug,
323
+ targetKey: entry.targetKey || `_interaction_${entry.slug}`,
324
+ question: entry.question,
325
+ };
326
+ }
327
+ }
328
+
329
+ setPendingInteraction(pending);
330
+ }, [history]);
331
+
332
+ const fetchData = async () => {
333
+ try {
334
+ const res = await fetch(historyUrl);
335
+ const data = await res.json();
336
+ if (data.entries) setHistory(data.entries);
337
+ if (data.workflowName) setWorkflowName(data.workflowName);
338
+
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
+ }
346
+
347
+ setLoading(false);
348
+ return true;
349
+ } catch (err) {
350
+ setError(err.message);
351
+ setLoading(false);
352
+ return false;
353
+ }
354
+ };
355
+
356
+ useEffect(() => {
220
357
  fetchData();
221
358
 
222
- // Setup SSE
223
- const eventSource = new EventSource('/api/events');
224
- eventSource.onmessage = (event) => {
225
- if (event.data === 'update') {
359
+ let eventSource = null;
360
+ let reconnectTimeout = null;
361
+ let pollInterval = null;
362
+ let reconnectAttempts = 0;
363
+
364
+ const connect = () => {
365
+ if (eventSource) eventSource.close();
366
+ eventSource = new EventSource(eventsUrl);
367
+
368
+ eventSource.onopen = () => {
369
+ setStatus('connected');
370
+ reconnectAttempts = 0;
226
371
  fetchData();
227
- }
372
+ };
373
+
374
+ eventSource.onerror = () => {
375
+ setStatus('disconnected');
376
+ eventSource.close();
377
+ const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000);
378
+ reconnectAttempts++;
379
+ reconnectTimeout = setTimeout(connect, delay);
380
+ };
381
+
382
+ eventSource.onmessage = (e) => {
383
+ try {
384
+ if (e.data === 'update') {
385
+ fetchData();
386
+ return;
387
+ }
388
+
389
+ const data = JSON.parse(e.data);
390
+ switch (data.type) {
391
+ case 'status':
392
+ setStatus(data.cliConnected ? 'connected' : 'disconnected');
393
+ if (data.workflowName) setWorkflowName(data.workflowName);
394
+ 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
+ );
402
+ if (hasDupe) return prev;
403
+ }
404
+ return [data, ...prev];
405
+ });
406
+ break;
407
+ case 'cli_connected':
408
+ case 'cli_reconnected': setStatus('connected'); break;
409
+ case 'cli_disconnected': setStatus('disconnected'); break;
410
+ }
411
+ } catch (err) { /* Not JSON or update ping */ }
412
+ };
228
413
  };
229
414
 
415
+ connect();
416
+ pollInterval = setInterval(fetchData, 10000);
417
+
230
418
  return () => {
231
- eventSource.close();
419
+ if (eventSource) eventSource.close();
420
+ if (reconnectTimeout) clearTimeout(reconnectTimeout);
421
+ if (pollInterval) clearInterval(pollInterval);
232
422
  };
233
423
  }, []);
234
424
 
235
- const toggleTheme = () => {
236
- setTheme(prev => prev === 'dark' ? 'light' : 'dark');
237
- };
425
+ const handleSubmit = async (slug, targetKey, response) => {
426
+ const optimisticEvent = {
427
+ timestamp: new Date().toISOString(),
428
+ event: 'INTERACTION_SUBMITTED',
429
+ slug,
430
+ targetKey,
431
+ answer: response.substring(0, 200) + (response.length > 200 ? '...' : ''),
432
+ source: 'remote',
433
+ };
434
+ setHistory(prev => [optimisticEvent, ...prev]);
238
435
 
239
- const toggleSort = () => {
240
- setSortOrder(prev => prev === 'newest' ? 'oldest' : 'newest');
436
+ try {
437
+ const res = await fetch(submitUrl, {
438
+ method: 'POST',
439
+ headers: { 'Content-Type': 'application/json' },
440
+ body: JSON.stringify({ slug, targetKey, response }),
441
+ });
442
+ 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');
446
+ }
447
+ setTimeout(fetchData, 1000);
448
+ } catch (err) {
449
+ setHistory(prev => prev.filter(e => e !== optimisticEvent));
450
+ alert(err.message);
451
+ }
241
452
  };
242
453
 
243
- if (loading) return (
244
- <div className={theme}>
245
- <div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-black text-gray-500 dark:text-zinc-500">
246
- Loading history...
247
- </div>
248
- </div>
249
- );
454
+ const toggleTheme = () => setTheme(prev => prev === 'dark' ? 'light' : 'dark');
455
+ const toggleSort = () => setSortNewest(prev => !prev);
250
456
 
251
- if (error) return (
457
+ if (loading && !history.length) return (
252
458
  <div className={theme}>
253
- <div className="flex items-center justify-center h-screen bg-gray-50 dark:bg-black text-red-500">
254
- Error: {error}
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...
255
461
  </div>
256
462
  </div>
257
463
  );
258
464
 
259
- // Filter for events we want to display - NOW INCLUDES EVERYTHING
260
- // We only filter out nulls or malformed entries if any
261
- let visibleEvents = history;
262
-
263
- // Apply Sort
264
- // History from API is "Newest First" (index 0 is latest)
265
- if (sortOrder === 'oldest') {
266
- visibleEvents = [...visibleEvents].reverse();
267
- }
268
-
465
+ let visibleEvents = sortNewest ? history : [...history].reverse();
269
466
  const formatTime = (ts) => new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
270
467
 
271
468
  return (
272
469
  <div className={theme}>
273
- <div className="min-h-screen bg-gray-50 dark:bg-black transition-colors duration-200">
274
- <div className="max-w-5xl mx-auto min-h-screen flex flex-col">
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">
275
472
 
276
473
  {/* Sticky Header */}
277
- <header className="sticky top-0 z-50 py-4 px-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-colors">
278
- <div className="flex-1">
279
- <h1 className="text-xl font-bold text-gray-800 dark:text-zinc-100 transition-colors uppercase tracking-tight">{workflowName}</h1>
280
- <p className="text-gray-500 dark:text-zinc-500 text-xs mt-0.5">Runtimes History & Prompt Logs</p>
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'}
482
+ </div>
483
+ <div className="w-1 h-1 rounded-full bg-zinc-300 dark:bg-zinc-800"></div>
484
+ <StatusBadge status={status} />
485
+ </div>
281
486
  </div>
282
- <div className="flex items-center space-x-2">
487
+ <div className="flex items-center space-x-3">
283
488
  <button
284
489
  onClick={toggleSort}
285
- className="p-2 rounded-full bg-gray-200 dark:bg-zinc-900 text-gray-800 dark:text-zinc-200 hover:bg-gray-300 dark:hover:bg-zinc-800 transition-colors"
286
- title={sortOrder === 'newest' ? "Sort: Newest First" : "Sort: Oldest First"}
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"}
287
492
  >
288
- {sortOrder === 'newest' ?
289
- <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" /></svg>
290
- :
291
- <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4h13M3 8h9m-9 4h6m4 0l4-4m0 0l4 4m-4-4v12" transform="scale(1, -1) translate(0, -24)" /></svg>
292
- }
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>
293
496
  </button>
294
497
  <button
295
498
  onClick={toggleTheme}
296
- className="p-2 rounded-full bg-gray-200 dark:bg-zinc-900 text-gray-800 dark:text-zinc-200 hover:bg-gray-300 dark:hover:bg-zinc-800 transition-colors"
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"
297
500
  title="Toggle Theme"
298
501
  >
299
502
  {theme === 'dark' ? <SunIcon /> : <MoonIcon />}
@@ -301,10 +504,30 @@
301
504
  </div>
302
505
  </header>
303
506
 
507
+ {/* Pending Interaction at Top */}
508
+ {pendingInteraction && (
509
+ <InteractionForm
510
+ interaction={pendingInteraction}
511
+ onSubmit={handleSubmit}
512
+ disabled={status !== 'connected'}
513
+ />
514
+ )}
515
+
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...
521
+ </div>
522
+ </div>
523
+ )}
524
+
304
525
  {/* Content */}
305
- <div className="flex-1 space-y-8 px-6">
526
+ <div className="flex-1 space-y-12">
306
527
  {visibleEvents.length === 0 && (
307
- <div className="text-center text-gray-400 py-10">No execution history found.</div>
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...
530
+ </div>
308
531
  )}
309
532
 
310
533
  {visibleEvents.map((item, idx) => {
@@ -312,19 +535,21 @@
312
535
  if (item.event.startsWith('WORKFLOW_')) {
313
536
  const colorMap = {
314
537
  'WORKFLOW_STARTED': 'text-green-500 dark:text-green-400',
315
- 'WORKFLOW_COMPLETED': 'text-blue-500 dark:text-blue-400',
538
+ 'WORKFLOW_COMPLETED': 'text-blue-510 dark:text-blue-400',
316
539
  'WORKFLOW_FAILED': 'text-red-500 dark:text-red-400',
317
540
  'WORKFLOW_RESET': 'text-yellow-500 dark:text-yellow-400'
318
541
  };
319
542
  return (
320
543
  <div key={idx} className="flex flex-col items-center py-4">
321
- <div className="flex items-center space-x-3 text-[10px] uppercase tracking-[0.2em] font-bold text-zinc-400 dark:text-zinc-600">
322
- <div className="h-px w-8 bg-zinc-200 dark:bg-zinc-800"></div>
323
- <span className={colorMap[item.event]}>{item.event.replace('_', ' ')}</span>
324
- <span>{formatTime(item.timestamp)}</span>
325
- <div className="h-px w-8 bg-zinc-200 dark:bg-zinc-800"></div>
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>
326
551
  </div>
327
- {item.error && <div className="mt-2 text-red-500 text-xs font-mono">{item.error}</div>}
552
+ {item.error && <div className="mt-2 text-red-500 text-xs font-mono max-w-md text-center">{item.error}</div>}
328
553
  </div>
329
554
  );
330
555
  }
@@ -333,9 +558,9 @@
333
558
  if (item.event === 'AGENT_STARTED') {
334
559
  return (
335
560
  <div key={idx} className="flex justify-start">
336
- <div className="text-[10px] text-zinc-400 dark:text-zinc-600 uppercase tracking-widest flex items-center space-x-2">
561
+ <div className="text-[10px] text-zinc-400 dark:text-zinc-600 uppercase tracking-widest font-bold flex items-center space-x-2">
337
562
  <span className="w-1.5 h-1.5 rounded-full bg-blue-500/50 animate-pulse"></span>
338
- <span>Agent <span className="text-zinc-600 dark:text-zinc-400">{item.agent}</span> started</span>
563
+ <span>Agent <span className="text-zinc-800 dark:text-zinc-300">{item.agent}</span> started</span>
339
564
  <span>&bull;</span>
340
565
  <span>{formatTime(item.timestamp)}</span>
341
566
  </div>
@@ -347,9 +572,9 @@
347
572
  if (item.event === 'AGENT_FAILED') {
348
573
  return (
349
574
  <div key={idx} className="flex justify-center">
350
- <div className="bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-900/50 rounded-lg px-4 py-2 text-red-600 dark:text-red-400 text-xs font-mono w-full max-w-2xl">
351
- <div className="font-bold mb-1 underline">AGENT FAILED: {item.agent}</div>
352
- <div>{item.error}</div>
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>
353
578
  </div>
354
579
  </div>
355
580
  );
@@ -357,14 +582,14 @@
357
582
 
358
583
  // 4. Interaction / Prompt Requested
359
584
  if (item.event === 'INTERACTION_REQUESTED' || item.event === 'PROMPT_REQUESTED') {
360
- const isPrompt = item.event === 'PROMPT_REQUESTED';
361
585
  return (
362
- <div key={idx} className="flex justify-center">
363
- <div className="bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 border-dashed rounded-lg px-6 py-4 text-center max-w-md w-full">
364
- <div className="text-[10px] text-zinc-400 dark:text-zinc-600 uppercase font-bold tracking-widest mb-1">
365
- {isPrompt ? 'User Input Requested' : 'Human Intervention Needed'}
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
366
591
  </div>
367
- <div className="text-xs text-zinc-600 dark:text-zinc-400 italic">
592
+ <div className="text-xs text-zinc-600 dark:text-zinc-400 italic font-medium leading-relaxed">
368
593
  {item.question ? `"${item.question}"` : `Waiting for response to "${item.slug}"...`}
369
594
  </div>
370
595
  </div>
@@ -373,14 +598,15 @@
373
598
  }
374
599
 
375
600
  // 5. Prompt Answered
376
- if (item.event === 'PROMPT_ANSWERED') {
601
+ if (item.event === 'PROMPT_ANSWERED' || item.event === 'INTERACTION_SUBMITTED') {
602
+ const isManual = item.source === 'remote';
377
603
  return (
378
604
  <div key={idx} className="flex justify-center">
379
- <div className="bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-900/50 rounded-lg px-4 py-2 text-center max-w-md w-full">
380
- <div className="text-[10px] text-green-600 dark:text-green-400 uppercase font-bold tracking-widest mb-1">
381
- User Answered
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'}
382
608
  </div>
383
- <div className="text-xs text-green-700 dark:text-green-300 italic font-medium">
609
+ <div className="text-xs text-green-800 dark:text-green-300 italic font-bold">
384
610
  "{item.answer}"
385
611
  </div>
386
612
  </div>
@@ -388,20 +614,19 @@
388
614
  );
389
615
  }
390
616
 
391
- // 6. Agent Completed / Interaction Resolved (The Bubbles)
617
+ // 6. Agent Completed / Interaction Resolved
392
618
  if (item.event === 'AGENT_COMPLETED' || item.event === 'INTERACTION_RESOLVED') {
393
619
  return (
394
- <div key={idx} className="flex flex-col space-y-4">
620
+ <div key={idx} className="flex flex-col space-y-6">
395
621
  {/* Header Line */}
396
- <div className="flex items-center justify-center space-x-2 text-[10px] text-zinc-400 dark:text-zinc-600 uppercase tracking-widest">
397
- <span className="font-black text-zinc-500 dark:text-zinc-400">{item.agent || item.slug}</span>
398
- <span>&bull;</span>
399
- <span>COMPLETED</span>
400
- <span>&bull;</span>
401
- <span>{formatTime(item.timestamp)}</span>
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>
402
627
  </div>
403
628
 
404
- {/* Output (Response) - NOW ON TOP */}
629
+ {/* Output (Response) - ON TOP */}
405
630
  {(item.output || item.result) && (
406
631
  <JsonView
407
632
  data={item.output || item.result}
@@ -410,13 +635,13 @@
410
635
  />
411
636
  )}
412
637
 
413
- {/* Prompt (Input) - NOW ON BOTTOM */}
638
+ {/* Prompt (Input) - ON BOTTOM */}
414
639
  {item.prompt && (
415
640
  <div className="flex justify-start w-full group">
416
- <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-6 transition-all hover:border-zinc-300 dark:hover:border-zinc-700 relative">
417
- <div className="flex justify-between items-center mb-3">
418
- <div className="text-[9px] font-black text-zinc-300 dark:text-zinc-700 uppercase tracking-[0.2em]">Prompt / Input</div>
419
- <div className="absolute top-4 right-4 opacity-0 group-hover:opacity-100 transition-opacity">
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">
420
645
  <CopyButton text={item.prompt} className="text-gray-400 hover:text-gray-600 dark:text-zinc-600 dark:hover:text-zinc-400" />
421
646
  </div>
422
647
  </div>
@@ -430,12 +655,12 @@
430
655
  );
431
656
  }
432
657
 
433
- // 7. CATCH-ALL for Unknown Events
658
+ // 7. CATCH-ALL
434
659
  return (
435
660
  <div key={idx} className="flex justify-center px-4">
436
661
  <JsonView
437
662
  data={JSON.parse(JSON.stringify(item, (key, value) => {
438
- if (key === 'event' || key === 'timestamp') return undefined; // Keep it clean
663
+ if (key === 'event' || key === 'timestamp') return undefined;
439
664
  return value;
440
665
  }))}
441
666
  label={item.event}
@@ -446,8 +671,8 @@
446
671
  })}
447
672
  </div>
448
673
 
449
- <footer className="mt-20 mb-8 text-center text-zinc-400 dark:text-zinc-800 text-[10px] uppercase tracking-[0.3em] transition-colors">
450
- Agent State Machine &bull; Debug Terminal
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
451
676
  </footer>
452
677
  </div>
453
678
  </div>
@@ -460,4 +685,4 @@
460
685
  </script>
461
686
  </body>
462
687
 
463
- </html>
688
+ </html>