agent-state-machine 2.0.15 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/bin/cli.js +1 -1
  2. package/lib/index.js +33 -0
  3. package/lib/remote/client.js +7 -2
  4. package/lib/runtime/agent.js +102 -67
  5. package/lib/runtime/index.js +13 -0
  6. package/lib/runtime/interaction.js +304 -0
  7. package/lib/runtime/prompt.js +39 -12
  8. package/lib/runtime/runtime.js +11 -10
  9. package/package.json +1 -1
  10. package/templates/project-builder/agents/assumptions-clarifier.md +0 -1
  11. package/templates/project-builder/agents/code-reviewer.md +0 -1
  12. package/templates/project-builder/agents/code-writer.md +0 -1
  13. package/templates/project-builder/agents/requirements-clarifier.md +0 -1
  14. package/templates/project-builder/agents/response-interpreter.md +25 -0
  15. package/templates/project-builder/agents/roadmap-generator.md +0 -1
  16. package/templates/project-builder/agents/sanity-checker.md +45 -0
  17. package/templates/project-builder/agents/sanity-runner.js +161 -0
  18. package/templates/project-builder/agents/scope-clarifier.md +0 -1
  19. package/templates/project-builder/agents/security-clarifier.md +0 -1
  20. package/templates/project-builder/agents/security-reviewer.md +0 -1
  21. package/templates/project-builder/agents/task-planner.md +0 -1
  22. package/templates/project-builder/agents/test-planner.md +0 -1
  23. package/templates/project-builder/scripts/interaction-helpers.js +33 -0
  24. package/templates/project-builder/scripts/workflow-helpers.js +2 -47
  25. package/templates/project-builder/workflow.js +214 -54
  26. package/vercel-server/api/session/[token].js +3 -3
  27. package/vercel-server/api/submit/[token].js +5 -3
  28. package/vercel-server/local-server.js +33 -6
  29. package/vercel-server/public/remote/index.html +17 -0
  30. package/vercel-server/ui/index.html +9 -1012
  31. package/vercel-server/ui/package-lock.json +2650 -0
  32. package/vercel-server/ui/package.json +25 -0
  33. package/vercel-server/ui/postcss.config.js +6 -0
  34. package/vercel-server/ui/src/App.jsx +236 -0
  35. package/vercel-server/ui/src/components/ChoiceInteraction.jsx +127 -0
  36. package/vercel-server/ui/src/components/ConfirmInteraction.jsx +51 -0
  37. package/vercel-server/ui/src/components/ContentCard.jsx +161 -0
  38. package/vercel-server/ui/src/components/CopyButton.jsx +27 -0
  39. package/vercel-server/ui/src/components/EventsLog.jsx +82 -0
  40. package/vercel-server/ui/src/components/Footer.jsx +66 -0
  41. package/vercel-server/ui/src/components/Header.jsx +38 -0
  42. package/vercel-server/ui/src/components/InteractionForm.jsx +42 -0
  43. package/vercel-server/ui/src/components/TextInteraction.jsx +72 -0
  44. package/vercel-server/ui/src/index.css +145 -0
  45. package/vercel-server/ui/src/main.jsx +8 -0
  46. package/vercel-server/ui/tailwind.config.js +19 -0
  47. package/vercel-server/ui/vite.config.js +11 -0
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "remote-follow-ui",
3
+ "private": true,
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "build": "vite build",
8
+ "preview": "vite preview"
9
+ },
10
+ "dependencies": {
11
+ "@fontsource-variable/inter": "^5.2.5",
12
+ "@fontsource-variable/jetbrains-mono": "^5.2.5",
13
+ "framer-motion": "^10.16.4",
14
+ "lucide-react": "^0.469.0",
15
+ "react": "^18.2.0",
16
+ "react-dom": "^18.2.0"
17
+ },
18
+ "devDependencies": {
19
+ "@vitejs/plugin-react": "^4.2.0",
20
+ "autoprefixer": "^10.4.18",
21
+ "postcss": "^8.4.35",
22
+ "tailwindcss": "^3.4.1",
23
+ "vite": "^5.1.4"
24
+ }
25
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {}
5
+ }
6
+ };
@@ -0,0 +1,236 @@
1
+ import { AnimatePresence, motion } from "framer-motion";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import ContentCard from "./components/ContentCard.jsx";
4
+ import EventsLog from "./components/EventsLog.jsx";
5
+ import Footer from "./components/Footer.jsx";
6
+ import Header from "./components/Header.jsx";
7
+ import InteractionForm from "./components/InteractionForm.jsx";
8
+
9
+ export default function App() {
10
+ const [history, setHistory] = useState([]);
11
+ const [pageIndex, setPageIndex] = useState(0);
12
+ const [status, setStatus] = useState("connecting");
13
+ const [workflowName, setWorkflowName] = useState("...");
14
+ const [theme, setTheme] = useState("light");
15
+ const [viewMode, setViewMode] = useState("present");
16
+ const [pendingInteraction, setPendingInteraction] = useState(null);
17
+ const [hasNew, setHasNew] = useState(false);
18
+
19
+ useEffect(() => {
20
+ const savedTheme = localStorage.getItem("rf_theme") || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
21
+ setTheme(savedTheme);
22
+ const savedView = localStorage.getItem("rf_view") || "present";
23
+ setViewMode(savedView);
24
+ }, []);
25
+
26
+ useEffect(() => {
27
+ document.documentElement.classList.toggle("dark", theme === "dark");
28
+ localStorage.setItem("rf_theme", theme);
29
+ }, [theme]);
30
+
31
+ useEffect(() => {
32
+ localStorage.setItem("rf_view", viewMode);
33
+ }, [viewMode]);
34
+
35
+ const token = window.SESSION_TOKEN === "{{" + "SESSION_TOKEN" + "}}" ? null : window.SESSION_TOKEN;
36
+ const historyUrl = token ? `/api/history/${token}` : "/api/history";
37
+ const eventsUrl = token ? `/api/events/${token}` : "/api/events";
38
+ const submitUrl = token ? `/api/submit/${token}` : "/api/submit";
39
+
40
+ const fetchData = async () => {
41
+ try {
42
+ const res = await fetch(historyUrl);
43
+ const data = await res.json();
44
+ if (data.entries) {
45
+ const chronological = [...data.entries].reverse();
46
+ setHistory((prev) => {
47
+ if (prev.length === 0 && chronological.length > 0) {
48
+ setPageIndex(chronological.length - 1);
49
+ }
50
+ return chronological;
51
+ });
52
+ const last = chronological[chronological.length - 1];
53
+ if (last && (last.event === "INTERACTION_REQUESTED" || last.event === "PROMPT_REQUESTED")) {
54
+ setPendingInteraction(last);
55
+ } else {
56
+ setPendingInteraction(null);
57
+ }
58
+ }
59
+ if (data.workflowName) setWorkflowName(data.workflowName);
60
+ setStatus("connected");
61
+ } catch (error) {
62
+ setStatus("disconnected");
63
+ }
64
+ };
65
+
66
+ const prevLen = useRef(0);
67
+ useEffect(() => {
68
+ if (history.length > prevLen.current) {
69
+ if (prevLen.current === 0 && history.length > 0) {
70
+ setPageIndex(history.length - 1);
71
+ setHasNew(false);
72
+ } else if (pageIndex < history.length - 1) {
73
+ setHasNew(true);
74
+ }
75
+ }
76
+ prevLen.current = history.length;
77
+ }, [history.length, pageIndex]);
78
+
79
+ useEffect(() => {
80
+ if (pageIndex === history.length - 1) {
81
+ setHasNew(false);
82
+ }
83
+ }, [pageIndex, history.length]);
84
+
85
+ useEffect(() => {
86
+ fetchData();
87
+ const interval = setInterval(fetchData, 3000);
88
+ const es = new EventSource(eventsUrl);
89
+ es.onmessage = (event) => {
90
+ if (event.data === "update") fetchData();
91
+ };
92
+ return () => {
93
+ clearInterval(interval);
94
+ es.close();
95
+ };
96
+ }, []);
97
+
98
+ const next = () => setPageIndex((prev) => Math.min(history.length - 1, prev + 1));
99
+ const prev = () => setPageIndex((prev) => Math.max(0, prev - 1));
100
+ const touchStart = useRef(null);
101
+ const ignoreTouch = useRef(false);
102
+
103
+ const handleTouchStart = (event) => {
104
+ const target = event.target;
105
+ const isInput = target.tagName === "TEXTAREA" || (target.tagName === "INPUT" && target.type === "number");
106
+ if (isInput && target.className !== "jumper-input") {
107
+ ignoreTouch.current = true;
108
+ return;
109
+ }
110
+ ignoreTouch.current = false;
111
+ const touch = event.touches[0];
112
+ touchStart.current = { x: touch.clientX, y: touch.clientY };
113
+ };
114
+
115
+ const handleTouchEnd = (event) => {
116
+ if (ignoreTouch.current || !touchStart.current) return;
117
+ const touch = event.changedTouches[0];
118
+ const dx = touch.clientX - touchStart.current.x;
119
+ const dy = touch.clientY - touchStart.current.y;
120
+ const absX = Math.abs(dx);
121
+ const absY = Math.abs(dy);
122
+ const swipeThreshold = 50;
123
+ if (absX > swipeThreshold && absX > absY + 10) {
124
+ if (dx > 0) prev();
125
+ if (dx < 0) next();
126
+ }
127
+ touchStart.current = null;
128
+ };
129
+
130
+ useEffect(() => {
131
+ const handler = (event) => {
132
+ const isInput = event.target.tagName === "TEXTAREA" || (event.target.tagName === "INPUT" && event.target.type === "number");
133
+ if (isInput && event.target.className !== "jumper-input") return;
134
+ if (event.key === "ArrowRight") next();
135
+ if (event.key === "ArrowLeft") prev();
136
+ };
137
+ window.addEventListener("keydown", handler);
138
+ return () => window.removeEventListener("keydown", handler);
139
+ }, [history.length]);
140
+
141
+ const currentItem = history[pageIndex];
142
+ const isRequestEvent = currentItem && (currentItem.event === "INTERACTION_REQUESTED" || currentItem.event === "PROMPT_REQUESTED");
143
+ const isRequest = pendingInteraction && isRequestEvent && currentItem.slug === pendingInteraction.slug;
144
+
145
+ return (
146
+ <div className="w-full h-[100dvh] flex flex-col relative overflow-hidden bg-bg">
147
+ <Header
148
+ workflowName={workflowName}
149
+ status={status}
150
+ theme={theme}
151
+ toggleTheme={() => setTheme((value) => (value === "dark" ? "light" : "dark"))}
152
+ viewMode={viewMode}
153
+ setViewMode={setViewMode}
154
+ history={history}
155
+ />
156
+
157
+ <main className="main-stage overflow-hidden">
158
+ {viewMode === "log" ? (
159
+ <EventsLog
160
+ history={history}
161
+ onJump={(idx) => {
162
+ setPageIndex(idx);
163
+ setViewMode("present");
164
+ }}
165
+ />
166
+ ) : (
167
+ <AnimatePresence mode="wait">
168
+ <motion.div
169
+ key={pageIndex}
170
+ drag="x"
171
+ dragConstraints={{ left: 0, right: 0 }}
172
+ dragElastic={0.2}
173
+ style={{ touchAction: "pan-y" }}
174
+ onTouchStart={handleTouchStart}
175
+ onTouchEnd={handleTouchEnd}
176
+ onDragEnd={(e, { offset, velocity }) => {
177
+ const swipeThreshold = 50;
178
+ if (offset.x > swipeThreshold || velocity.x > 500) {
179
+ prev();
180
+ } else if (offset.x < -swipeThreshold || velocity.x < -500) {
181
+ next();
182
+ }
183
+ }}
184
+ initial={{ opacity: 0, scale: 0.99, filter: "blur(4px)" }}
185
+ animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
186
+ exit={{ opacity: 0, scale: 1.01, filter: "blur(4px)" }}
187
+ transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
188
+ className="w-full h-full"
189
+ >
190
+ {isRequest ? (
191
+ <div className="content-width h-full">
192
+ <InteractionForm
193
+ interaction={pendingInteraction}
194
+ onSubmit={async (slug, targetKey, response) => {
195
+ const responsePreview = typeof response === "string" ? response : JSON.stringify(response);
196
+ setHistory((prev) => [
197
+ ...prev,
198
+ {
199
+ timestamp: new Date().toISOString(),
200
+ event: "INTERACTION_SUBMITTED",
201
+ answer: responsePreview,
202
+ response
203
+ }
204
+ ]);
205
+ setPageIndex((prev) => prev + 1);
206
+ await fetch(submitUrl, {
207
+ method: "POST",
208
+ headers: { "Content-Type": "application/json" },
209
+ body: JSON.stringify({ slug, targetKey, response })
210
+ });
211
+ setTimeout(fetchData, 1000);
212
+ }}
213
+ disabled={status === "disconnected"}
214
+ />
215
+ </div>
216
+ ) : (
217
+ <ContentCard item={currentItem} />
218
+ )}
219
+ </motion.div>
220
+ </AnimatePresence>
221
+ )}
222
+ </main>
223
+
224
+ <Footer
225
+ page={pageIndex}
226
+ total={history.length}
227
+ onNext={next}
228
+ onPrev={prev}
229
+ onJump={setPageIndex}
230
+ hasNew={hasNew}
231
+ onJumpToLatest={() => setPageIndex(history.length - 1)}
232
+ className={viewMode === "log" ? "opacity-0 pointer-events-none" : "opacity-100"}
233
+ />
234
+ </div>
235
+ );
236
+ }
@@ -0,0 +1,127 @@
1
+ import { useMemo, useState } from "react";
2
+ import { Bot, Check } from "lucide-react";
3
+
4
+ export default function ChoiceInteraction({ interaction, onSubmit, disabled }) {
5
+ const { prompt, question, options = [], multiSelect, allowCustom } = interaction;
6
+ const [selected, setSelected] = useState(multiSelect ? [] : null);
7
+ const [customText, setCustomText] = useState("");
8
+ const [showCustom, setShowCustom] = useState(false);
9
+
10
+ const list = useMemo(() => options || [], [options]);
11
+ const title = prompt || question || "Choose an option.";
12
+
13
+ const handleSelect = (key) => {
14
+ if (multiSelect) {
15
+ setSelected((prev) => (
16
+ prev.includes(key) ? prev.filter((item) => item !== key) : [...prev, key]
17
+ ));
18
+ setShowCustom(false);
19
+ } else {
20
+ setSelected(key);
21
+ setShowCustom(false);
22
+ }
23
+ };
24
+
25
+ const handleSubmit = () => {
26
+ if (showCustom && customText.trim()) {
27
+ onSubmit({ isCustom: true, customText: customText.trim(), raw: customText.trim() });
28
+ return;
29
+ }
30
+ if (multiSelect && selected.length > 0) {
31
+ onSubmit({ selectedKeys: selected, raw: selected.join(", ") });
32
+ return;
33
+ }
34
+ if (selected) {
35
+ onSubmit({ selectedKey: selected, raw: String(selected) });
36
+ }
37
+ };
38
+
39
+ const isValid = showCustom
40
+ ? Boolean(customText.trim())
41
+ : (multiSelect ? selected.length > 0 : Boolean(selected));
42
+
43
+ return (
44
+ <div className="w-full h-full flex flex-col items-stretch overflow-hidden">
45
+ <div className="flex-1 overflow-y-auto custom-scroll px-6 py-12 space-y-8 flex flex-col items-center">
46
+ <div className="space-y-4 shrink-0">
47
+ <div className="w-16 h-16 rounded-3xl bg-accent text-white flex items-center justify-center mx-auto shadow-2xl shadow-accent/40">
48
+ <Bot className="w-8 h-8" />
49
+ </div>
50
+ <h3 className="text-4xl font-extrabold tracking-tight text-fg pt-4 text-center">Choose an option.</h3>
51
+ </div>
52
+
53
+ <div className="text-xl font-medium text-fg/70 text-center max-w-2xl whitespace-pre-wrap">
54
+ {title}
55
+ </div>
56
+
57
+ <div className="w-full max-w-2xl space-y-3">
58
+ {list.map((opt) => {
59
+ const isSelected = multiSelect ? selected.includes(opt.key) : selected === opt.key;
60
+ return (
61
+ <button
62
+ key={opt.key}
63
+ onClick={() => handleSelect(opt.key)}
64
+ disabled={disabled}
65
+ type="button"
66
+ className={`w-full p-6 rounded-2xl border-2 transition-all text-left ${
67
+ isSelected
68
+ ? "border-accent bg-accent/10"
69
+ : "border-white/10 hover:border-white/20 bg-black/[0.03] dark:bg-white/[0.03]"
70
+ }`}
71
+ >
72
+ <div className="flex items-center gap-4">
73
+ <div className={`w-6 h-6 rounded-full border-2 flex items-center justify-center ${
74
+ isSelected
75
+ ? "border-accent bg-accent text-white"
76
+ : "border-white/20"
77
+ }`}>
78
+ {isSelected && <Check className="w-4 h-4" />}
79
+ </div>
80
+ <div className="flex-1">
81
+ <div className="font-bold text-lg">{opt.label || opt.key}</div>
82
+ {opt.description && <div className="text-sm text-fg/50 mt-1">{opt.description}</div>}
83
+ </div>
84
+ </div>
85
+ </button>
86
+ );
87
+ })}
88
+
89
+ {allowCustom && (
90
+ <button
91
+ onClick={() => { setShowCustom(true); setSelected(multiSelect ? [] : null); }}
92
+ disabled={disabled}
93
+ type="button"
94
+ className={`w-full p-6 rounded-2xl border-2 transition-all text-left ${
95
+ showCustom
96
+ ? "border-accent bg-accent/10"
97
+ : "border-white/10 hover:border-white/20 bg-black/[0.03] dark:bg-white/[0.03]"
98
+ }`}
99
+ >
100
+ <div className="font-bold text-lg">Other</div>
101
+ <div className="text-sm text-fg/50 mt-1">Provide a custom response</div>
102
+ </button>
103
+ )}
104
+
105
+ {showCustom && (
106
+ <textarea
107
+ value={customText}
108
+ onChange={(event) => setCustomText(event.target.value)}
109
+ placeholder="Type your response..."
110
+ className="w-full h-32 p-6 rounded-2xl bg-black/[0.03] dark:bg-white/[0.03] border-2 border-accent/30 focus:border-accent focus:outline-none text-lg"
111
+ />
112
+ )}
113
+ </div>
114
+ </div>
115
+
116
+ <div className="p-4 flex justify-center bg-gradient-to-t from-bg via-bg to-transparent shrink-0 border-t border-white/5">
117
+ <button
118
+ onClick={handleSubmit}
119
+ disabled={disabled || !isValid}
120
+ className="px-12 py-6 bg-fg text-bg rounded-full font-bold text-xl hover:scale-105 active:scale-95 transition-all disabled:opacity-30 shadow-2xl"
121
+ >
122
+ Continue
123
+ </button>
124
+ </div>
125
+ </div>
126
+ );
127
+ }
@@ -0,0 +1,51 @@
1
+ import { Bot } from "lucide-react";
2
+
3
+ export default function ConfirmInteraction({ interaction, onSubmit, disabled }) {
4
+ const {
5
+ prompt,
6
+ question,
7
+ confirmLabel = "Confirm",
8
+ cancelLabel = "Cancel",
9
+ context
10
+ } = interaction;
11
+
12
+ return (
13
+ <div className="w-full h-full flex flex-col items-stretch overflow-hidden">
14
+ <div className="flex-1 overflow-y-auto custom-scroll px-6 py-12 space-y-8 flex flex-col items-center justify-center">
15
+ <div className="space-y-4">
16
+ <div className="w-16 h-16 rounded-3xl bg-accent text-white flex items-center justify-center mx-auto shadow-2xl shadow-accent/40">
17
+ <Bot className="w-8 h-8" />
18
+ </div>
19
+ <h3 className="text-4xl font-extrabold tracking-tight text-fg pt-4 text-center">Confirm action.</h3>
20
+ </div>
21
+
22
+ <div className="text-xl font-medium text-fg/70 text-center max-w-2xl whitespace-pre-wrap">
23
+ {prompt || question || "Please confirm."}
24
+ </div>
25
+
26
+ {context?.documentPath && (
27
+ <div className="text-sm text-fg/40 text-center">
28
+ Review: <code className="bg-white/10 px-2 py-1 rounded">{context.documentPath}</code>
29
+ </div>
30
+ )}
31
+ </div>
32
+
33
+ <div className="p-4 flex justify-center gap-4 bg-gradient-to-t from-bg via-bg to-transparent shrink-0 border-t border-white/5">
34
+ <button
35
+ onClick={() => onSubmit({ confirmed: false, raw: cancelLabel })}
36
+ disabled={disabled}
37
+ className="px-12 py-6 bg-white/10 text-fg rounded-full font-bold text-xl hover:bg-white/20 transition-all disabled:opacity-30"
38
+ >
39
+ {cancelLabel}
40
+ </button>
41
+ <button
42
+ onClick={() => onSubmit({ confirmed: true, raw: confirmLabel })}
43
+ disabled={disabled}
44
+ className="px-12 py-6 bg-fg text-bg rounded-full font-bold text-xl hover:scale-105 active:scale-95 transition-all disabled:opacity-30 shadow-2xl"
45
+ >
46
+ {confirmLabel}
47
+ </button>
48
+ </div>
49
+ </div>
50
+ );
51
+ }
@@ -0,0 +1,161 @@
1
+ import CopyButton from "./CopyButton.jsx";
2
+
3
+ export default function ContentCard({ item }) {
4
+ if (!item) return null;
5
+ const time = new Date(item.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" });
6
+ const eventLabel = item.event ? item.event.replace(/_/g, " ") : "EVENT";
7
+
8
+ const renderValue = (value) => {
9
+ if (value === null || value === undefined) {
10
+ return <span className="opacity-40">—</span>;
11
+ }
12
+ if (typeof value === "string") {
13
+ const trimmed = value.trim();
14
+ const looksLikeJson = trimmed.startsWith("{") || trimmed.startsWith("[");
15
+ if (looksLikeJson) {
16
+ try {
17
+ const parsed = JSON.parse(trimmed);
18
+ return (
19
+ <pre className="text-sm font-mono opacity-80 leading-relaxed custom-scroll overflow-auto bg-black/[0.015] dark:bg-white/[0.015] p-6 rounded-[24px] border border-border">
20
+ {JSON.stringify(parsed, null, 2)}
21
+ </pre>
22
+ );
23
+ } catch (error) {
24
+ return <span className="text-2xl font-semibold whitespace-pre-wrap break-words">{value}</span>;
25
+ }
26
+ }
27
+ if (value.length > 140 || value.includes("\n")) {
28
+ return (
29
+ <pre className="text-sm font-mono opacity-80 leading-relaxed custom-scroll overflow-auto bg-black/[0.015] dark:bg-white/[0.015] p-6 rounded-[24px] border border-border">
30
+ {value}
31
+ </pre>
32
+ );
33
+ }
34
+ return <span className="text-2xl font-semibold whitespace-pre-wrap break-words">{value}</span>;
35
+ }
36
+ if (typeof value === "object") {
37
+ return (
38
+ <pre className="text-sm font-mono opacity-80 leading-relaxed custom-scroll overflow-auto bg-black/[0.015] dark:bg-white/[0.015] p-6 rounded-[24px] border border-border">
39
+ {JSON.stringify(value, null, 2)}
40
+ </pre>
41
+ );
42
+ }
43
+ return <span className="text-2xl font-semibold">{String(value)}</span>;
44
+ };
45
+
46
+ const renderKeyValueGrid = (entries) => (
47
+ <div className="space-y-8">
48
+ {entries.map(([key, value]) => (
49
+ <div key={key} className="space-y-2">
50
+ <div className="text-[10px] font-bold tracking-[0.35em] uppercase opacity-40">
51
+ {key.replace(/_/g, " ")}
52
+ </div>
53
+ <div className="text-base">{renderValue(value)}</div>
54
+ </div>
55
+ ))}
56
+ </div>
57
+ );
58
+
59
+ let content = null;
60
+
61
+ if (item.event === "AGENT_STARTED") {
62
+ content = (
63
+ <div className="space-y-12 py-24">
64
+ <div className="space-y-3 text-center">
65
+ <div className="text-xs font-bold tracking-[0.4em] uppercase opacity-40">Agent invocation</div>
66
+ <h2 className="text-4xl font-black tracking-tight">{item.agent}</h2>
67
+ <div className="text-xs font-mono opacity-20">{time}</div>
68
+ </div>
69
+ <div className="markdown-body opacity-80 leading-relaxed text-xl font-light whitespace-pre-wrap">
70
+ {item.prompt}
71
+ </div>
72
+ </div>
73
+ );
74
+ } else if (item.event && item.event.startsWith("WORKFLOW_")) {
75
+ const step = item.event.replace("WORKFLOW_", "");
76
+ content = (
77
+ <div className="min-h-[50vh] flex flex-col items-center justify-center text-center space-y-8 py-20">
78
+ <div className="text-[10px] font-bold tracking-[0.5em] uppercase opacity-20">Lifecycle status</div>
79
+ <h2 className="text-8xl font-black tracking-tighter opacity-10 leading-none uppercase">{step}</h2>
80
+ {item.error && <pre className="text-red-500 font-mono text-xs max-w-xl whitespace-pre-wrap bg-red-50 dark:bg-red-900/5 p-6 rounded-[24px] mt-4">{item.error}</pre>}
81
+ </div>
82
+ );
83
+ } else if (item.event === "PROMPT_REQUESTED" || item.event === "INTERACTION_REQUESTED") {
84
+ const details = [
85
+ ["slug", item.slug],
86
+ ["targetKey", item.targetKey],
87
+ ["type", item.type]
88
+ ].filter((entry) => entry[1] !== undefined);
89
+
90
+ content = (
91
+ <div className="space-y-10 py-24">
92
+ <div className="space-y-3">
93
+ <div className="text-xs font-bold tracking-[0.4em] uppercase text-accent">Awaiting response</div>
94
+ <div className="text-xs font-mono opacity-20">{time}</div>
95
+ </div>
96
+ <div className="text-4xl font-bold tracking-tight text-balance leading-tight">
97
+ {item.question || item.prompt || "Prompt"}
98
+ </div>
99
+ {item.prompt && item.question && item.prompt !== item.question && (
100
+ <div className="text-lg font-medium opacity-70 whitespace-pre-wrap">{item.prompt}</div>
101
+ )}
102
+ {details.length > 0 && (
103
+ <div className="pt-4">
104
+ {renderKeyValueGrid(details)}
105
+ </div>
106
+ )}
107
+ </div>
108
+ );
109
+ } else if (item.event === "PROMPT_ANSWERED" || item.event === "INTERACTION_SUBMITTED") {
110
+ const responseValue = item.answer !== undefined ? item.answer : item.response;
111
+ const details = [
112
+ ["slug", item.slug],
113
+ ["targetKey", item.targetKey]
114
+ ].filter((entry) => entry[1] !== undefined);
115
+
116
+ content = (
117
+ <div className="space-y-10 py-24">
118
+ <div className="space-y-3">
119
+ <div className="text-xs font-bold tracking-[0.4em] uppercase text-accent">Response recorded</div>
120
+ <div className="text-xs font-mono opacity-20">{time}</div>
121
+ </div>
122
+ <div className="text-4xl font-bold tracking-tight text-balance leading-tight">
123
+ {renderValue(responseValue)}
124
+ </div>
125
+ {details.length > 0 && (
126
+ <div className="pt-4">
127
+ {renderKeyValueGrid(details)}
128
+ </div>
129
+ )}
130
+ </div>
131
+ );
132
+ } else {
133
+ const entries = Object.entries(item).filter(([key]) => key !== "event" && key !== "timestamp");
134
+ const fallbackEntries = entries.length > 0 ? entries : [["event", item.event || "Event"]];
135
+
136
+ content = (
137
+ <div className="space-y-12 py-24">
138
+ <div className="flex items-center justify-between">
139
+ <div className="text-xs font-bold tracking-[0.4em] uppercase opacity-40">{eventLabel}</div>
140
+ <div className="flex items-center gap-6">
141
+ <span className="text-xs font-mono opacity-20">{time}</span>
142
+ <CopyButton text={item} />
143
+ </div>
144
+ </div>
145
+ {renderKeyValueGrid(fallbackEntries)}
146
+ </div>
147
+ );
148
+ }
149
+
150
+ return (
151
+ <div className="w-full h-full flex flex-col overflow-y-auto custom-scroll px-12">
152
+ <div className="content-width flex-1">
153
+ <div className="flex items-center justify-between pt-10">
154
+ <div className="text-[10px] font-bold tracking-[0.4em] uppercase opacity-30">{eventLabel}</div>
155
+ <CopyButton text={item} />
156
+ </div>
157
+ {content}
158
+ </div>
159
+ </div>
160
+ );
161
+ }
@@ -0,0 +1,27 @@
1
+ import { useState } from "react";
2
+ import { Check, Copy } from "lucide-react";
3
+
4
+ export default function CopyButton({ text, label = "Copy", disabled = false }) {
5
+ const [copied, setCopied] = useState(false);
6
+
7
+ const handleCopy = (event) => {
8
+ event.stopPropagation();
9
+ if (disabled) return;
10
+ const content = typeof text === "string" ? text : JSON.stringify(text, null, 2);
11
+ navigator.clipboard.writeText(content);
12
+ setCopied(true);
13
+ setTimeout(() => setCopied(false), 2000);
14
+ };
15
+
16
+ return (
17
+ <button
18
+ onClick={handleCopy}
19
+ className={`tooltip p-2 rounded-full text-subtle transition-colors ${disabled ? "opacity-30 cursor-not-allowed" : "hover:bg-black/5 dark:hover:bg-white/10"}`}
20
+ data-tooltip={label}
21
+ aria-label={label}
22
+ disabled={disabled}
23
+ >
24
+ {copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
25
+ </button>
26
+ );
27
+ }