agent-state-machine 2.2.1 → 2.2.2

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 (31) hide show
  1. package/bin/cli.js +30 -2
  2. package/lib/runtime/agent.js +6 -2
  3. package/lib/runtime/interaction.js +2 -1
  4. package/lib/runtime/prompt.js +37 -1
  5. package/lib/runtime/runtime.js +67 -5
  6. package/package.json +1 -1
  7. package/templates/project-builder/agents/code-fixer.md +50 -0
  8. package/templates/project-builder/agents/code-writer.md +3 -0
  9. package/templates/project-builder/agents/sanity-checker.md +6 -0
  10. package/templates/project-builder/agents/test-planner.md +3 -1
  11. package/templates/project-builder/config.js +4 -4
  12. package/templates/project-builder/scripts/workflow-helpers.js +104 -2
  13. package/templates/project-builder/workflow.js +151 -14
  14. package/templates/starter/config.js +1 -1
  15. package/vercel-server/api/submit/[token].js +0 -11
  16. package/vercel-server/local-server.js +0 -19
  17. package/vercel-server/public/remote/assets/index-BsJsLDKc.css +1 -0
  18. package/vercel-server/public/remote/assets/index-CmtT6ADh.js +168 -0
  19. package/vercel-server/public/remote/index.html +2 -2
  20. package/vercel-server/ui/src/App.jsx +69 -19
  21. package/vercel-server/ui/src/components/ChoiceInteraction.jsx +69 -18
  22. package/vercel-server/ui/src/components/ConfirmInteraction.jsx +7 -7
  23. package/vercel-server/ui/src/components/ContentCard.jsx +600 -104
  24. package/vercel-server/ui/src/components/EventsLog.jsx +20 -13
  25. package/vercel-server/ui/src/components/Footer.jsx +9 -4
  26. package/vercel-server/ui/src/components/Header.jsx +12 -3
  27. package/vercel-server/ui/src/components/SendingCard.jsx +33 -0
  28. package/vercel-server/ui/src/components/TextInteraction.jsx +8 -8
  29. package/vercel-server/ui/src/index.css +82 -10
  30. package/vercel-server/public/remote/assets/index-CbgeVnKw.js +0 -148
  31. package/vercel-server/public/remote/assets/index-DHL_iHQW.css +0 -1
@@ -1,5 +1,36 @@
1
+ import { useEffect, useState } from "react";
1
2
  import CopyButton from "./CopyButton.jsx";
2
- import { Bot, Brain, ChevronRight } from "lucide-react";
3
+ import { Bot, Brain, ChevronRight, Search, X } from "lucide-react";
4
+
5
+ const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6
+
7
+ const highlightText = (text, query) => {
8
+ const source = String(text ?? "");
9
+ const q = (query || "").trim();
10
+ if (!q) return source;
11
+ const regex = new RegExp(escapeRegExp(q), "gi");
12
+ const matches = source.match(regex);
13
+ if (!matches) return source;
14
+
15
+ const parts = source.split(regex);
16
+ const nodes = [];
17
+
18
+ parts.forEach((part, index) => {
19
+ nodes.push(part);
20
+ if (index < matches.length) {
21
+ nodes.push(
22
+ <mark
23
+ key={`mark-${index}`}
24
+ className="rounded bg-black/10 dark:bg-white/20 px-1"
25
+ >
26
+ {matches[index]}
27
+ </mark>
28
+ );
29
+ }
30
+ });
31
+
32
+ return nodes;
33
+ };
3
34
 
4
35
  function AgentStartedIcon({ className = "" }) {
5
36
  return (
@@ -12,7 +43,107 @@ function AgentStartedIcon({ className = "" }) {
12
43
  );
13
44
  }
14
45
 
15
- export default function ContentCard({ item }) {
46
+ function MonoBlock({ children }) {
47
+ return (
48
+ <pre className="text-xs sm:text-sm font-mono leading-relaxed whitespace-pre-wrap break-words overflow-auto max-h-[60vh] p-6 rounded-[24px] border border-black bg-black text-white/90 dark:border-white dark:bg-white dark:text-black/90">
49
+ {children}
50
+ </pre>
51
+ );
52
+ }
53
+
54
+ function TextBlock({ children }) {
55
+ return (
56
+ <div className="rounded-[24px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04] p-6 text-base sm:text-lg leading-relaxed whitespace-pre-wrap break-words text-black/80 dark:text-white/80">
57
+ {children}
58
+ </div>
59
+ );
60
+ }
61
+
62
+ function PanelBody({ as: Component = "div", className = "", children }) {
63
+ return (
64
+ <Component
65
+ className={`border-t border-black/10 dark:border-white/10 p-6 ${className}`}
66
+ >
67
+ {children}
68
+ </Component>
69
+ );
70
+ }
71
+
72
+ function RawToggle({ open, onToggle }) {
73
+ return (
74
+ <button
75
+ type="button"
76
+ onClick={onToggle}
77
+ className="px-3 py-1 rounded-full text-[10px] font-bold tracking-[0.2em] uppercase border border-black/20 dark:border-white/20 text-black/60 dark:text-white/60 hover:text-black dark:hover:text-white hover:border-black/40 dark:hover:border-white/40 transition-colors"
78
+ aria-pressed={open}
79
+ aria-label={open ? "Hide raw event JSON" : "Show raw event JSON"}
80
+ >
81
+ Raw
82
+ </button>
83
+ );
84
+ }
85
+
86
+ const renderInlineValue = (value) => {
87
+ if (value === null || value === undefined) {
88
+ return <span className="italic">—</span>;
89
+ }
90
+ if (typeof value === "string") {
91
+ if (value.length > 140 || value.includes("\n")) {
92
+ return <TextBlock>{value}</TextBlock>;
93
+ }
94
+ return (
95
+ <span className="text-lg sm:text-xl font-semibold whitespace-pre-wrap break-words">
96
+ {value}
97
+ </span>
98
+ );
99
+ }
100
+ if (typeof value === "object") {
101
+ return <pre className="raw-json-block raw-json-block--full">{renderJsonWithHighlight(value)}</pre>;
102
+ }
103
+ return <span className="text-lg sm:text-xl font-semibold break-words">{String(value)}</span>;
104
+ };
105
+
106
+ function renderJsonWithHighlight(value) {
107
+ const raw = JSON.stringify(value, null, 2);
108
+ const regex =
109
+ /("(?:\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*"(?:\s*:)?|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g;
110
+ const nodes = [];
111
+ let lastIndex = 0;
112
+ let match;
113
+
114
+ while ((match = regex.exec(raw)) !== null) {
115
+ const { index } = match;
116
+ if (index > lastIndex) {
117
+ nodes.push(raw.slice(lastIndex, index));
118
+ }
119
+
120
+ const token = match[0];
121
+ let className = "json-token-number";
122
+ if (token.startsWith('"')) {
123
+ className = token.endsWith(":") ? "json-token-key" : "json-token-string";
124
+ } else if (token === "true" || token === "false") {
125
+ className = "json-token-boolean";
126
+ } else if (token === "null") {
127
+ className = "json-token-null";
128
+ }
129
+
130
+ nodes.push(
131
+ <span key={`json-${index}`} className={className}>
132
+ {token}
133
+ </span>
134
+ );
135
+
136
+ lastIndex = index + token.length;
137
+ }
138
+
139
+ if (lastIndex < raw.length) {
140
+ nodes.push(raw.slice(lastIndex));
141
+ }
142
+
143
+ return nodes;
144
+ }
145
+
146
+ export default function ContentCard({ item, promptSearchRequestId = 0 }) {
16
147
  if (!item) return null;
17
148
 
18
149
  const time = new Date(item.timestamp).toLocaleTimeString([], {
@@ -21,10 +152,42 @@ export default function ContentCard({ item }) {
21
152
  second: "2-digit",
22
153
  });
23
154
 
24
- const eventLabel = item.event ? item.event.replace(/_/g, " ") : "EVENT";
25
-
26
- const formatKey = (key) =>
27
- key.replace(/_/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2");
155
+ const [promptQuery, setPromptQuery] = useState("");
156
+ const [showRaw, setShowRaw] = useState(false);
157
+ const [showPromptSearch, setShowPromptSearch] = useState(false);
158
+
159
+ // Full-auto countdown logic (must be at top level for hooks rules)
160
+ const isInteractionEvent = item.event === "PROMPT_REQUESTED" || item.event === "INTERACTION_REQUESTED";
161
+ const isFullAuto = isInteractionEvent && !!item.fullAuto;
162
+ const interactionType = item.type || "text";
163
+ const interactionOptions = item.options || [];
164
+ const autoSelectDelay = item.autoSelectDelay ?? 20;
165
+ const shouldShowCountdown = isFullAuto && interactionType === "choice" && interactionOptions.length > 0;
166
+
167
+ // Calculate countdown directly - triggers re-render via tick state
168
+ const [tick, setTick] = useState(0);
169
+
170
+ const countdown = (() => {
171
+ if (!shouldShowCountdown) return null;
172
+ const eventTime = new Date(item.timestamp).getTime();
173
+ const elapsed = Math.floor((Date.now() - eventTime) / 1000);
174
+ return autoSelectDelay - elapsed;
175
+ })();
176
+
177
+ useEffect(() => {
178
+ if (!shouldShowCountdown) return;
179
+
180
+ const timer = setInterval(() => {
181
+ setTick(t => t + 1); // Force re-render to recalculate countdown
182
+ }, 1000);
183
+
184
+ return () => clearInterval(timer);
185
+ }, [shouldShowCountdown, item.timestamp]);
186
+
187
+ const formatKey = (key) => {
188
+ const spaced = key.replace(/_/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2");
189
+ return spaced.replace(/\b\w/g, (char) => char.toUpperCase());
190
+ };
28
191
 
29
192
  const tryParseJson = (raw) => {
30
193
  try {
@@ -109,11 +272,73 @@ export default function ContentCard({ item }) {
109
272
  };
110
273
  };
111
274
 
112
- const MonoBlock = ({ children }) => (
113
- <pre className="text-sm font-mono leading-relaxed overflow-auto p-6 rounded-[24px] border border-black bg-black text-white dark:border-white dark:bg-white dark:text-black">
114
- {children}
115
- </pre>
116
- );
275
+ const splitPromptSections = (promptText) => {
276
+ if (typeof promptText !== "string" || !promptText.trim()) return [];
277
+ const lines = promptText.split(/\r?\n/);
278
+ const sections = [];
279
+ let current = null;
280
+ let inFence = false;
281
+
282
+ const pushSection = () => {
283
+ if (!current) return;
284
+ const content = current.content.join("\n").replace(/\n{3,}/g, "\n\n").trim();
285
+ if (!content) {
286
+ current = null;
287
+ return;
288
+ }
289
+ sections.push({ title: current.title || "Prompt", content });
290
+ current = null;
291
+ };
292
+
293
+ lines.forEach((line) => {
294
+ const trimmed = line.trim();
295
+ if (/^```/.test(trimmed)) {
296
+ inFence = !inFence;
297
+ }
298
+ if (!trimmed) {
299
+ if (current) current.content.push("");
300
+ return;
301
+ }
302
+ if (!inFence && /^---+$/.test(trimmed)) {
303
+ pushSection();
304
+ return;
305
+ }
306
+ const headingMatch = !inFence ? /^(#{1,6})\s+(.+)$/.exec(trimmed) : null;
307
+ if (headingMatch) {
308
+ pushSection();
309
+ current = { title: headingMatch[2].trim(), content: [] };
310
+ return;
311
+ }
312
+ if (!current) current = { title: "Prompt", content: [] };
313
+ current.content.push(line);
314
+ });
315
+
316
+ pushSection();
317
+ return sections;
318
+ };
319
+
320
+ const renderPromptSectionContent = (content, query) => {
321
+ if (!content) {
322
+ return (
323
+ <PanelBody className="italic text-black/40 dark:text-white/40">—</PanelBody>
324
+ );
325
+ }
326
+ if (content.includes("```")) {
327
+ return (
328
+ <PanelBody
329
+ as="pre"
330
+ className="text-xs sm:text-sm font-mono leading-relaxed whitespace-pre-wrap break-words overflow-auto max-h-[60vh] text-black/90 dark:text-white/90"
331
+ >
332
+ {highlightText(content, query)}
333
+ </PanelBody>
334
+ );
335
+ }
336
+ return (
337
+ <PanelBody className="text-base sm:text-lg leading-relaxed whitespace-pre-wrap break-words text-black/80 dark:text-white/80">
338
+ {highlightText(content, query)}
339
+ </PanelBody>
340
+ );
341
+ };
117
342
 
118
343
  const renderValue = (value) => {
119
344
  if (value === null || value === undefined) {
@@ -122,7 +347,7 @@ export default function ContentCard({ item }) {
122
347
 
123
348
  if (typeof value === "boolean") {
124
349
  const base =
125
- "inline-flex items-center rounded-full px-3 py-1 text-xs font-semibold tracking-[0.2em] uppercase border";
350
+ "inline-flex items-center rounded-full px-3 py-1 text-[11px] font-semibold tracking-[0.16em] uppercase border";
126
351
  return value ? (
127
352
  <span className={`${base} bg-black text-white border-black dark:bg-white dark:text-black dark:border-white`}>
128
353
  Yes
@@ -137,15 +362,15 @@ export default function ContentCard({ item }) {
137
362
  if (typeof value === "string") {
138
363
  const parsedJson = extractJsonFromString(value);
139
364
  if (parsedJson !== null) {
140
- return <MonoBlock>{JSON.stringify(parsedJson, null, 2)}</MonoBlock>;
365
+ return renderStructured(parsedJson);
141
366
  }
142
367
 
143
368
  if (value.length > 140 || value.includes("\n")) {
144
- return <MonoBlock>{value}</MonoBlock>;
369
+ return <TextBlock>{value}</TextBlock>;
145
370
  }
146
371
 
147
372
  return (
148
- <span className="text-2xl font-semibold whitespace-pre-wrap break-words">
373
+ <span className="text-lg sm:text-xl font-semibold whitespace-pre-wrap break-words">
149
374
  {value}
150
375
  </span>
151
376
  );
@@ -155,7 +380,7 @@ export default function ContentCard({ item }) {
155
380
  return <MonoBlock>{JSON.stringify(value, null, 2)}</MonoBlock>;
156
381
  }
157
382
 
158
- return <span className="text-2xl font-semibold">{String(value)}</span>;
383
+ return <span className="text-lg sm:text-xl font-semibold break-words">{String(value)}</span>;
159
384
  };
160
385
 
161
386
  const MAX_STRUCT_DEPTH = 6;
@@ -181,9 +406,9 @@ export default function ContentCard({ item }) {
181
406
 
182
407
  if (isSimple) {
183
408
  return (
184
- <div className="space-y-2">
409
+ <div className="space-y-3">
185
410
  {items.map((entry, index) => (
186
- <div key={`item-${index}`} className="text-base">
411
+ <div key={`item-${index}`} className="text-base sm:text-lg">
187
412
  {renderValue(entry)}
188
413
  </div>
189
414
  ))}
@@ -197,7 +422,7 @@ export default function ContentCard({ item }) {
197
422
  {items.map((entry, index) => (
198
423
  <div
199
424
  key={`item-${index}`}
200
- className="rounded-[20px] border border-black p-4 dark:border-white"
425
+ className="rounded-[20px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04] p-4"
201
426
  >
202
427
  {renderKeyValueGrid(Object.entries(entry), depth)}
203
428
  </div>
@@ -211,9 +436,9 @@ export default function ContentCard({ item }) {
211
436
  {items.map((entry, index) => (
212
437
  <div
213
438
  key={`item-${index}`}
214
- className="space-y-4 rounded-[24px] border border-black p-5 dark:border-white"
439
+ className="space-y-4 rounded-[24px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04] p-5"
215
440
  >
216
- <div className="text-[10px] font-bold tracking-[0.35em] uppercase">
441
+ <div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/50 dark:text-white/50">
217
442
  Item {index + 1}
218
443
  </div>
219
444
  {renderStructured(entry, depth + 1)}
@@ -245,13 +470,15 @@ export default function ContentCard({ item }) {
245
470
 
246
471
  function renderKeyValueGrid(entries, depth = 0) {
247
472
  return (
248
- <div className="space-y-8">
473
+ <div className="space-y-6">
249
474
  {entries.map(([key, value]) => (
250
- <div key={key} className="space-y-2">
251
- <div className="text-[10px] font-bold tracking-[0.35em] uppercase">
475
+ <div key={key} className="grid gap-2 sm:grid-cols-[160px_1fr] sm:items-start min-w-0">
476
+ <div className="text-xs font-semibold text-black/50 dark:text-white/50 break-words">
252
477
  {formatKey(key)}
253
478
  </div>
254
- <div className="text-base">{renderStructured(value, depth + 1)}</div>
479
+ <div className="text-base sm:text-lg leading-relaxed break-words min-w-0">
480
+ {renderStructured(value, depth + 1)}
481
+ </div>
255
482
  </div>
256
483
  ))}
257
484
  </div>
@@ -260,140 +487,400 @@ export default function ContentCard({ item }) {
260
487
 
261
488
  let content = null;
262
489
 
490
+ useEffect(() => {
491
+ if (promptSearchRequestId > 0) {
492
+ setShowPromptSearch(true);
493
+ }
494
+ }, [promptSearchRequestId]);
495
+
263
496
  if (item.event === "AGENT_STARTED") {
264
497
  const { cleanedPrompt, contextTitle, contextData, contextRaw } = extractContextFromPrompt(
265
498
  item.prompt || ""
266
499
  );
500
+ const promptSections = splitPromptSections(cleanedPrompt);
501
+ const trimmedPromptQuery = promptQuery.trim();
502
+ const normalizedPromptQuery = trimmedPromptQuery.toLowerCase();
503
+ const filteredPromptSections = trimmedPromptQuery
504
+ ? promptSections.filter((section) =>
505
+ `${section.title}\n${section.content}`.toLowerCase().includes(normalizedPromptQuery)
506
+ )
507
+ : [];
508
+ const promptCountLabel = promptSections.length ? `${promptSections.length} sections` : null;
267
509
 
268
510
  const contextTokenCount = estimateTokensFromText(contextRaw || "");
269
511
  const contextMeta = contextRaw ? `~${formatTokenCount(contextTokenCount)}` : "";
270
512
 
271
513
  content = (
272
- <div className="space-y-12 py-4">
273
- <div className="space-y-4 text-center">
514
+ <div className="space-y-10 py-6">
515
+ <div className="space-y-3 text-center">
274
516
  <AgentStartedIcon />
275
- <div className="space-y-2">
276
- <h2 className="text-4xl font-black tracking-tight">{item.agent}</h2>
277
- <div className="text-xs font-mono">{time}</div>
517
+ <div className="space-y-1">
518
+ <div className="text-[11px] font-semibold tracking-[0.28em] uppercase text-black/50 dark:text-white/50">
519
+ Agent started
520
+ </div>
521
+ <h2 className="text-3xl sm:text-4xl font-black tracking-tight">{item.agent}</h2>
278
522
  </div>
279
523
  </div>
280
524
 
281
525
  {contextTitle && contextData !== null && (
282
- <details className="group rounded-[24px] border border-black dark:border-white">
526
+ <details className="group rounded-[24px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04]">
283
527
  <summary className="cursor-pointer select-none px-6 py-5 flex items-center justify-between gap-6 [&::-webkit-details-marker]:hidden">
284
528
  <div className="flex items-center gap-3">
285
529
  <ChevronRight className="w-4 h-4 transition-transform group-open:rotate-90" />
286
530
  {contextTitle === "Current Context" ? (
287
531
  <Brain className="w-4 h-4" aria-hidden="true" />
288
532
  ) : null}
289
- <div className="text-sm font-semibold tracking-[0.12em] uppercase">
533
+ <div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/60 dark:text-white/60">
290
534
  {contextTitle}
291
535
  </div>
292
536
  </div>
293
- {contextMeta ? <div className="text-xs font-mono">{contextMeta}</div> : null}
537
+ {contextMeta ? <div className="text-xs font-mono text-black/50 dark:text-white/50">{contextMeta}</div> : null}
294
538
  </summary>
295
539
 
296
- <div className="border-t border-black dark:border-white p-6">
297
- {typeof contextData === "string" ? (
298
- <MonoBlock>{contextData}</MonoBlock>
299
- ) : (
300
- renderStructured(contextData)
301
- )}
302
- </div>
540
+ {typeof contextData === "string" ? (
541
+ <PanelBody className="text-base sm:text-lg leading-relaxed whitespace-pre-wrap break-words text-black/80 dark:text-white/80">
542
+ {contextData}
543
+ </PanelBody>
544
+ ) : (
545
+ <PanelBody>{renderStructured(contextData)}</PanelBody>
546
+ )}
303
547
  </details>
304
548
  )}
305
549
 
306
- <div className="markdown-body leading-relaxed text-xl font-light whitespace-pre-wrap [&_*]:text-inherit [&_a]:underline">
307
- {cleanedPrompt}
550
+ <div className="space-y-4">
551
+ <div className="flex flex-wrap items-center justify-between gap-3">
552
+ <div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/50 dark:text-white/50">
553
+ Prompt
554
+ </div>
555
+ {promptCountLabel ? (
556
+ <div className="text-xs font-mono text-black/40 dark:text-white/40">
557
+ {promptCountLabel}
558
+ </div>
559
+ ) : null}
560
+ </div>
561
+
562
+ <div className="space-y-4">
563
+ {promptSections.length > 0 ? (
564
+ promptSections.map((section, index) => {
565
+ const isInstructions = /(^| )instructions$/i.test(section.title.trim());
566
+ const shouldOpen = index === 0 || isInstructions;
567
+ const lineCount = section.content
568
+ ? section.content.split(/\r?\n/).length
569
+ : 0;
570
+ return (
571
+ <details
572
+ key={`${section.title}-${index}`}
573
+ open={shouldOpen}
574
+ className="group rounded-[24px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04]"
575
+ >
576
+ <summary className="cursor-pointer select-none px-6 py-5 flex items-center justify-between gap-6 [&::-webkit-details-marker]:hidden">
577
+ <div className="flex items-center gap-3 min-w-0">
578
+ <ChevronRight className="w-4 h-4 transition-transform group-open:rotate-90" />
579
+ <div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/60 dark:text-white/60 break-words">
580
+ {section.title || "Prompt"}
581
+ </div>
582
+ </div>
583
+ {lineCount ? (
584
+ <div className="text-xs font-mono text-black/40 dark:text-white/40">
585
+ {lineCount} lines
586
+ </div>
587
+ ) : null}
588
+ </summary>
589
+ {renderPromptSectionContent(section.content, "")}
590
+ </details>
591
+ );
592
+ })
593
+ ) : (
594
+ <TextBlock>{cleanedPrompt}</TextBlock>
595
+ )}
596
+ </div>
308
597
  </div>
598
+
599
+ {showPromptSearch ? (
600
+ <div className="fixed inset-0 z-40 bg-white text-black dark:bg-black dark:text-white">
601
+ <div className="h-full w-full overflow-y-auto custom-scroll px-6 sm:px-10 py-10">
602
+ <div className="max-w-3xl mx-auto space-y-8">
603
+ <div className="flex flex-wrap items-center justify-between gap-4">
604
+ <div className="text-[11px] font-semibold tracking-[0.32em] uppercase text-black/50 dark:text-white/50">
605
+ Prompt search
606
+ </div>
607
+ <button
608
+ type="button"
609
+ onClick={() => setShowPromptSearch(false)}
610
+ className="text-[10px] font-bold tracking-[0.2em] uppercase text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white"
611
+ >
612
+ Close
613
+ </button>
614
+ </div>
615
+
616
+ <div className="relative">
617
+ <Search className="w-4 h-4 text-black/50 dark:text-white/60 absolute left-4 top-1/2 -translate-y-1/2" />
618
+ <input
619
+ value={promptQuery}
620
+ onChange={(event) => setPromptQuery(event.target.value)}
621
+ placeholder="Search the prompt..."
622
+ className="w-full rounded-full border border-black/20 dark:border-white/20 bg-transparent py-4 pl-11 pr-10 text-base text-black dark:text-white placeholder:text-black/40 dark:placeholder:text-white/40 focus:outline-none focus:ring-1 focus:ring-black/30 dark:focus:ring-white/40"
623
+ autoFocus
624
+ />
625
+ {trimmedPromptQuery ? (
626
+ <button
627
+ type="button"
628
+ onClick={() => setPromptQuery("")}
629
+ aria-label="Clear prompt search"
630
+ className="absolute right-3 top-1/2 -translate-y-1/2 rounded-full p-1 text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white"
631
+ >
632
+ <X className="w-3.5 h-3.5" />
633
+ </button>
634
+ ) : null}
635
+ </div>
636
+
637
+ {trimmedPromptQuery ? (
638
+ filteredPromptSections.length > 0 ? (
639
+ <div className="space-y-4">
640
+ {filteredPromptSections.map((section, index) => {
641
+ const lineCount = section.content
642
+ ? section.content.split(/\r?\n/).length
643
+ : 0;
644
+ return (
645
+ <div
646
+ key={`${section.title}-${index}`}
647
+ className="rounded-[22px] border border-black/10 dark:border-white/15 bg-black/[0.02] dark:bg-white/5 overflow-hidden"
648
+ >
649
+ <div className="px-5 py-4 flex items-center justify-between gap-4">
650
+ <div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/70 dark:text-white/70 break-words">
651
+ {highlightText(section.title || "Prompt", trimmedPromptQuery)}
652
+ </div>
653
+ {lineCount ? (
654
+ <div className="text-xs font-mono text-black/50 dark:text-white/50">
655
+ {lineCount} lines
656
+ </div>
657
+ ) : null}
658
+ </div>
659
+ <div className="border-t border-black/10 dark:border-white/10 p-5">
660
+ <pre className="m-0 text-xs sm:text-sm font-mono leading-relaxed whitespace-pre-wrap break-words text-black/85 dark:text-white/85">
661
+ {highlightText(section.content || "", trimmedPromptQuery)}
662
+ </pre>
663
+ </div>
664
+ </div>
665
+ );
666
+ })}
667
+ </div>
668
+ ) : (
669
+ <div className="rounded-[20px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/5 p-5 text-sm text-black/60 dark:text-white/60">
670
+ No prompt sections match "{trimmedPromptQuery}".
671
+ </div>
672
+ )
673
+ ) : (
674
+ <div className="text-sm text-black/50 dark:text-white/50">
675
+ Start typing to filter prompt sections.
676
+ </div>
677
+ )}
678
+ </div>
679
+ </div>
680
+ </div>
681
+ ) : null}
309
682
  </div>
310
683
  );
311
684
  } else if (item.event && item.event.startsWith("WORKFLOW_")) {
312
685
  const step = item.event.replace("WORKFLOW_", "");
313
686
  content = (
314
- <div className="min-h-[50vh] flex flex-col items-center justify-center text-center space-y-8 py-20">
315
- <div className="text-[10px] font-bold tracking-[0.5em] uppercase">
687
+ <div className="min-h-[50vh] flex flex-col items-center justify-center text-center space-y-8 py-16">
688
+ <div className="text-[10px] font-semibold tracking-[0.4em] uppercase text-black/50 dark:text-white/50">
316
689
  Lifecycle status
317
690
  </div>
318
691
 
319
- <div className="w-full max-w-5xl rounded-[28px] border border-black p-10 dark:border-white">
320
- <h2 className="text-6xl sm:text-7xl md:text-8xl font-black tracking-tighter leading-none uppercase">
692
+ <div className="w-full max-w-5xl rounded-[28px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04] p-10">
693
+ <h2 className="text-4xl sm:text-6xl md:text-7xl font-black tracking-tighter leading-none uppercase">
321
694
  {step}
322
695
  </h2>
323
696
 
324
697
  {item.error && (
325
- <pre className="mt-8 text-xs font-mono whitespace-pre-wrap rounded-[24px] border border-black p-6 bg-black text-white dark:border-white dark:bg-white dark:text-black">
326
- {item.error}
327
- </pre>
698
+ <div className="mt-6">
699
+ <MonoBlock>{item.error}</MonoBlock>
700
+ </div>
328
701
  )}
329
702
  </div>
330
703
  </div>
331
704
  );
332
705
  } else if (item.event === "PROMPT_REQUESTED" || item.event === "INTERACTION_REQUESTED") {
333
- const details = [
334
- ["slug", item.slug],
335
- ["targetKey", item.targetKey],
336
- ["type", item.type],
337
- ].filter((entry) => entry[1] !== undefined);
706
+ const isInteraction = item.event === "INTERACTION_REQUESTED";
707
+ const label = isInteraction ? "Interaction needed" : "Question asked";
708
+ const headline = item.question || item.prompt || "Prompt";
709
+ const agentName = item.agent || item.slug || "Agent";
338
710
 
339
711
  content = (
340
- <div className="space-y-10 py-4">
341
- <div className="text-xs font-mono">{time}</div>
342
-
343
- <div className="text-4xl font-bold tracking-tight text-balance leading-tight">
344
- {item.question || item.prompt || "Prompt"}
712
+ <div className="py-10 sm:py-14 space-y-8">
713
+ <div className="text-center space-y-4">
714
+ <AgentStartedIcon className="w-12 h-12" />
715
+ <div className="text-[11px] font-semibold tracking-[0.32em] uppercase text-black/50 dark:text-white/50">
716
+ {label}
717
+ </div>
718
+ <div className="text-lg sm:text-xl font-semibold text-black/70 dark:text-white/70">
719
+ {agentName} needs your input
720
+ </div>
721
+ </div>
722
+ <div className="text-3xl sm:text-4xl md:text-5xl font-black tracking-tight leading-tight text-center text-balance">
723
+ {headline}
345
724
  </div>
346
725
 
347
- {item.prompt && item.question && item.prompt !== item.question && (
348
- <div className="text-lg font-medium whitespace-pre-wrap">{item.prompt}</div>
726
+ {shouldShowCountdown && countdown !== null && (
727
+ <div className="text-center">
728
+ <div className={`inline-flex items-center gap-2 px-4 py-2 rounded-full ${
729
+ countdown > 0
730
+ ? "bg-black/5 dark:bg-white/10"
731
+ : "bg-black text-white dark:bg-white dark:text-black"
732
+ }`}>
733
+ <span className="text-xl">⚡</span>
734
+ <span className={`text-sm font-semibold ${
735
+ countdown > 0 ? "text-black/70 dark:text-white/70" : ""
736
+ }`}>
737
+ {countdown > 0
738
+ ? `Agent deciding in ${countdown}s...`
739
+ : "Auto-selected"}
740
+ </span>
741
+ </div>
742
+ </div>
349
743
  )}
350
744
 
351
- {details.length > 0 && <div className="pt-4">{renderKeyValueGrid(details)}</div>}
745
+ {interactionType === "choice" && interactionOptions.length > 0 && (
746
+ <div className="space-y-3 max-w-2xl mx-auto">
747
+ <div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/50 dark:text-white/50 text-center">
748
+ Options
749
+ </div>
750
+ <div className="space-y-2">
751
+ {interactionOptions.map((opt, index) => (
752
+ <div
753
+ key={opt.key || index}
754
+ className={`p-4 rounded-2xl border transition-all ${
755
+ index === 0
756
+ ? "border-black bg-black text-white dark:border-white dark:bg-white dark:text-black"
757
+ : "border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.03]"
758
+ }`}
759
+ >
760
+ <div className="flex items-center gap-3">
761
+ <div className={`font-bold ${index === 0 ? "" : "text-black dark:text-white"}`}>
762
+ {opt.label || opt.key}
763
+ {index === 0 && (
764
+ <span className={`ml-2 text-xs font-medium px-2 py-0.5 rounded-full ${
765
+ "bg-white/20 dark:bg-black/20"
766
+ }`}>
767
+ Recommended
768
+ </span>
769
+ )}
770
+ </div>
771
+ </div>
772
+ {opt.description && (
773
+ <div className={`text-sm mt-1 ${
774
+ index === 0 ? "text-white/70 dark:text-black/70" : "text-black/50 dark:text-white/50"
775
+ }`}>
776
+ {opt.description}
777
+ </div>
778
+ )}
779
+ </div>
780
+ ))}
781
+ </div>
782
+ {item.allowCustom && (
783
+ <div className="text-xs text-center text-black/40 dark:text-white/40 mt-2">
784
+ Custom response allowed
785
+ </div>
786
+ )}
787
+ </div>
788
+ )}
352
789
  </div>
353
790
  );
354
- } else if (item.event === "PROMPT_ANSWERED" || item.event === "INTERACTION_SUBMITTED") {
355
- const responseValue = item.answer !== undefined ? item.answer : item.response;
356
-
357
- const details = [
358
- ["slug", item.slug],
359
- ["targetKey", item.targetKey],
360
- ].filter((entry) => entry[1] !== undefined);
791
+ } else if (item.event === "INTERACTION_AUTO_RESOLVED" || item.event === "PROMPT_AUTO_ANSWERED") {
792
+ const autoSelected = item.autoSelected || "Unknown";
793
+ const agentName = item.agent || item.slug || "Agent";
361
794
 
362
795
  content = (
363
- <div className="space-y-10 py-4">
364
- <div className="text-xs font-mono">{time}</div>
796
+ <div className="space-y-8 py-6">
797
+ <div className="space-y-4 text-center">
798
+ <div className="mx-auto w-14 h-14 rounded-full border-2 border-black dark:border-white bg-black dark:bg-white flex items-center justify-center">
799
+ <span className="text-2xl">⚡</span>
800
+ </div>
801
+ <div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/50 dark:text-white/50">
802
+ Auto-selected
803
+ </div>
804
+ <div className="text-lg sm:text-xl font-semibold text-black/70 dark:text-white/70">
805
+ {agentName} decided automatically
806
+ </div>
807
+ </div>
808
+ <div className="rounded-[24px] border-2 border-black bg-black text-white dark:border-white dark:bg-white dark:text-black p-6 text-center max-w-md mx-auto">
809
+ <div className="text-2xl sm:text-3xl font-black tracking-tight">
810
+ {autoSelected}
811
+ </div>
812
+ </div>
813
+ </div>
814
+ );
815
+ } else if (item.event === "INTERACTION_RESOLVED") {
816
+ const resolvedCopy =
817
+ item.source === "remote" ? "Remote response received." : "Response captured.";
818
+ const sourceCopy =
819
+ item.source === "remote" ? "Received from remote" : item.source ? `Received from ${item.source}` : "Received";
820
+ const agentName = item.agent || item.slug || "Agent";
365
821
 
366
- <div className="text-4xl font-bold tracking-tight text-balance leading-tight">
367
- {renderValue(responseValue)}
822
+ content = (
823
+ <div className="space-y-8 py-6">
824
+ <div className="space-y-4 text-center">
825
+ <AgentStartedIcon className="w-12 h-12" />
826
+ <div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/50 dark:text-white/50">
827
+ Interaction resolved
828
+ </div>
829
+ <div className="text-lg sm:text-xl font-semibold text-black/70 dark:text-white/70">
830
+ {agentName} received your response
831
+ </div>
368
832
  </div>
833
+ <div className="rounded-[24px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04] p-6 text-center">
834
+ <div className="text-xl sm:text-2xl font-semibold leading-relaxed text-balance">
835
+ {resolvedCopy}
836
+ </div>
837
+ <div className="mt-3 text-sm text-black/60 dark:text-white/60">
838
+ {sourceCopy}
839
+ </div>
840
+ </div>
841
+ </div>
842
+ );
843
+ } else if (item.event === "PROMPT_ANSWERED" || item.event === "INTERACTION_SUBMITTED") {
844
+ const responseValue = item.answer !== undefined ? item.answer : item.response;
369
845
 
370
- {details.length > 0 && <div className="pt-4">{renderKeyValueGrid(details)}</div>}
846
+ content = (
847
+ <div className="space-y-8 py-6">
848
+ <div className="space-y-3">
849
+ <div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/50 dark:text-white/50">
850
+ Response sent
851
+ </div>
852
+ {renderInlineValue(responseValue)}
853
+ </div>
371
854
  </div>
372
855
  );
373
856
  } else if (item.event === "AGENT_COMPLETED") {
374
- const details = [
375
- ["agent", item.agent],
376
- ["attempts", item.attempts],
377
- ].filter((entry) => entry[1] !== undefined);
378
-
379
857
  content = (
380
- <div className="space-y-12 py-4">
381
- {item.output !== undefined && (
382
- <div className="space-y-8">
383
- <div className="rounded-[28px] border border-black px-6 py-5 dark:border-white">
384
- <div className="flex flex-wrap items-center justify-between gap-3">
385
- <div className="text-3xl font-black tracking-tight">
386
- Agent <i>{item.agent ? " " + item.agent : ""}</i> responded
387
- </div>
388
- <div className="text-xs font-mono">{time}</div>
389
- </div>
858
+ <div className="space-y-10 py-6">
859
+ <div className="rounded-[28px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04] p-6 sm:p-8 text-center space-y-4">
860
+ <AgentStartedIcon className="w-12 h-12" />
861
+ <div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/50 dark:text-white/50">
862
+ Agent completed
863
+ </div>
864
+ <div className="text-2xl sm:text-3xl font-black tracking-tight">
865
+ {item.agent || "Agent"}
866
+ </div>
867
+ {typeof item.attempts === "number" ? (
868
+ <div className="text-sm text-black/60 dark:text-white/60">
869
+ {item.attempts} {item.attempts === 1 ? "attempt" : "attempts"}
390
870
  </div>
871
+ ) : null}
872
+ </div>
391
873
 
392
- {renderStructured(item.output)}
874
+ {item.output !== undefined ? (
875
+ <div className="space-y-4">
876
+ <div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/50 dark:text-white/50">
877
+ Answer
878
+ </div>
879
+ <div className="rounded-[24px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04] p-6">
880
+ {renderStructured(item.output)}
881
+ </div>
393
882
  </div>
394
- )}
395
-
396
- {details.length > 0 && <div className="pt-4">{renderKeyValueGrid(details)}</div>}
883
+ ) : null}
397
884
  </div>
398
885
  );
399
886
  } else {
@@ -401,26 +888,35 @@ export default function ContentCard({ item }) {
401
888
  ([key]) => key !== "event" && key !== "timestamp"
402
889
  );
403
890
 
404
- const fallbackEntries = entries.length > 0 ? entries : [["event", item.event || "Event"]];
405
-
406
891
  content = (
407
- <div className="space-y-12 py-4">
408
- <div className="text-xs font-mono">{time}</div>
409
- {renderKeyValueGrid(fallbackEntries)}
892
+ <div className="space-y-6 py-6">
893
+ <div className="rounded-[24px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04] p-6">
894
+ <div className="text-lg sm:text-xl font-semibold leading-relaxed text-balance">
895
+ {item.event ? item.event.replace(/_/g, " ") : "Event"}
896
+ </div>
897
+ <div className="mt-3 text-sm text-black/60 dark:text-white/60">
898
+ {entries.length > 0 ? "Open Raw for full details." : "Open Raw for more info."}
899
+ </div>
900
+ </div>
410
901
  </div>
411
902
  );
412
903
  }
413
904
 
414
905
  return (
415
- <div className="w-full h-full flex flex-col overflow-y-auto custom-scroll px-12 bg-white text-black dark:bg-black dark:text-white">
906
+ <div className="w-full h-full flex flex-col overflow-y-auto custom-scroll px-6 sm:px-10 lg:px-12 bg-white text-black dark:bg-black dark:text-white">
416
907
  <div className="content-width flex-1">
417
- <div className="flex items-center justify-between pt-10">
418
- <div className="text-[10px] font-bold tracking-[0.4em] uppercase">
419
- {eventLabel}
908
+ <div className="flex flex-wrap items-center justify-between gap-4 pt-8 sm:pt-10 pb-6 border-b border-black/10 dark:border-white/10">
909
+ <div className="text-xs font-mono text-black/50 dark:text-white/50">{time}</div>
910
+ <div className="flex items-center gap-3">
911
+ <RawToggle open={showRaw} onToggle={() => setShowRaw((prev) => !prev)} />
912
+ <CopyButton text={item} />
420
913
  </div>
421
- <CopyButton text={item} />
422
914
  </div>
423
- {content}
915
+ {showRaw ? (
916
+ <pre className="raw-json-block">{renderJsonWithHighlight(item)}</pre>
917
+ ) : (
918
+ content
919
+ )}
424
920
  </div>
425
921
  </div>
426
922
  );