agent-state-machine 2.2.0 → 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 (36) hide show
  1. package/bin/cli.js +78 -2
  2. package/lib/remote/client.js +37 -8
  3. package/lib/runtime/agent.js +6 -2
  4. package/lib/runtime/interaction.js +2 -1
  5. package/lib/runtime/prompt.js +37 -1
  6. package/lib/runtime/runtime.js +67 -5
  7. package/package.json +1 -1
  8. package/templates/project-builder/README.md +304 -56
  9. package/templates/project-builder/agents/code-fixer.md +50 -0
  10. package/templates/project-builder/agents/code-writer.md +3 -0
  11. package/templates/project-builder/agents/sanity-checker.md +6 -0
  12. package/templates/project-builder/agents/sanity-runner.js +3 -1
  13. package/templates/project-builder/agents/test-planner.md +3 -1
  14. package/templates/project-builder/config.js +4 -4
  15. package/templates/project-builder/scripts/workflow-helpers.js +104 -2
  16. package/templates/project-builder/workflow.js +151 -14
  17. package/templates/starter/README.md +291 -42
  18. package/templates/starter/config.js +1 -1
  19. package/vercel-server/api/submit/[token].js +2 -13
  20. package/vercel-server/api/ws/cli.js +40 -2
  21. package/vercel-server/local-server.js +32 -22
  22. package/vercel-server/public/remote/assets/index-BsJsLDKc.css +1 -0
  23. package/vercel-server/public/remote/assets/index-CmtT6ADh.js +168 -0
  24. package/vercel-server/public/remote/index.html +2 -2
  25. package/vercel-server/ui/src/App.jsx +69 -62
  26. package/vercel-server/ui/src/components/ChoiceInteraction.jsx +69 -18
  27. package/vercel-server/ui/src/components/ConfirmInteraction.jsx +7 -7
  28. package/vercel-server/ui/src/components/ContentCard.jsx +600 -104
  29. package/vercel-server/ui/src/components/EventsLog.jsx +20 -13
  30. package/vercel-server/ui/src/components/Footer.jsx +9 -4
  31. package/vercel-server/ui/src/components/Header.jsx +12 -3
  32. package/vercel-server/ui/src/components/SendingCard.jsx +33 -0
  33. package/vercel-server/ui/src/components/TextInteraction.jsx +8 -8
  34. package/vercel-server/ui/src/index.css +82 -10
  35. package/vercel-server/public/remote/assets/index-BOKpYANC.js +0 -148
  36. package/vercel-server/public/remote/assets/index-DHL_iHQW.css +0 -1
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
6
6
  <title>{{WORKFLOW_NAME}}</title>
7
- <script type="module" crossorigin src="/remote/assets/index-BOKpYANC.js"></script>
8
- <link rel="stylesheet" crossorigin href="/remote/assets/index-DHL_iHQW.css">
7
+ <script type="module" crossorigin src="/remote/assets/index-CmtT6ADh.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/remote/assets/index-BsJsLDKc.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -5,6 +5,8 @@ import EventsLog from "./components/EventsLog.jsx";
5
5
  import Footer from "./components/Footer.jsx";
6
6
  import Header from "./components/Header.jsx";
7
7
  import InteractionForm from "./components/InteractionForm.jsx";
8
+ import SendingCard from "./components/SendingCard.jsx";
9
+ import { Search } from "lucide-react";
8
10
 
9
11
  export default function App() {
10
12
  const [history, setHistory] = useState([]);
@@ -15,6 +17,8 @@ export default function App() {
15
17
  const [viewMode, setViewMode] = useState("present");
16
18
  const [pendingInteraction, setPendingInteraction] = useState(null);
17
19
  const [hasNew, setHasNew] = useState(false);
20
+ const [sendingState, setSendingState] = useState(null);
21
+ const [promptSearchRequestId, setPromptSearchRequestId] = useState(0);
18
22
 
19
23
  useEffect(() => {
20
24
  const savedTheme = localStorage.getItem("rf_theme") || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
@@ -76,6 +80,32 @@ export default function App() {
76
80
  prevLen.current = history.length;
77
81
  }, [history.length, pageIndex]);
78
82
 
83
+ useEffect(() => {
84
+ if (!sendingState || history.length === 0) return;
85
+ const latest = history[history.length - 1];
86
+ const hasNewEvent =
87
+ history.length !== sendingState.historyLength ||
88
+ (latest?.timestamp && latest.timestamp !== sendingState.lastEventTimestamp);
89
+
90
+ if (!hasNewEvent) return;
91
+
92
+ if (latest?.event === "INTERACTION_SUBMITTED") {
93
+ setSendingState((prev) =>
94
+ prev
95
+ ? {
96
+ ...prev,
97
+ historyLength: history.length,
98
+ lastEventTimestamp: latest?.timestamp || prev.lastEventTimestamp,
99
+ }
100
+ : prev
101
+ );
102
+ return;
103
+ }
104
+
105
+ setSendingState(null);
106
+ setPageIndex(history.length - 1);
107
+ }, [history, sendingState]);
108
+
79
109
  useEffect(() => {
80
110
  if (pageIndex === history.length - 1) {
81
111
  setHasNew(false);
@@ -97,35 +127,6 @@ export default function App() {
97
127
 
98
128
  const next = () => setPageIndex((prev) => Math.min(history.length - 1, prev + 1));
99
129
  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
 
130
131
  useEffect(() => {
131
132
  const handler = (event) => {
@@ -139,8 +140,10 @@ export default function App() {
139
140
  }, [history.length]);
140
141
 
141
142
  const currentItem = history[pageIndex];
143
+ const isAgentStarted = currentItem?.event === "AGENT_STARTED";
142
144
  const isRequestEvent = currentItem && (currentItem.event === "INTERACTION_REQUESTED" || currentItem.event === "PROMPT_REQUESTED");
143
145
  const isRequest = pendingInteraction && isRequestEvent && currentItem.slug === pendingInteraction.slug;
146
+ const isSending = Boolean(sendingState);
144
147
 
145
148
  return (
146
149
  <div className="w-full h-[100dvh] flex flex-col relative overflow-hidden bg-bg">
@@ -167,54 +170,46 @@ export default function App() {
167
170
  <AnimatePresence mode="wait">
168
171
  <motion.div
169
172
  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
173
  initial={{ opacity: 0, scale: 0.99, filter: "blur(4px)" }}
185
174
  animate={{ opacity: 1, scale: 1, filter: "blur(0px)" }}
186
175
  exit={{ opacity: 0, scale: 1.01, filter: "blur(4px)" }}
187
176
  transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
188
177
  className="w-full h-full"
189
178
  >
190
- {isRequest ? (
179
+ {isSending ? (
180
+ <SendingCard submission={sendingState} />
181
+ ) : isRequest ? (
191
182
  <div className="content-width h-full">
192
183
  <InteractionForm
193
184
  interaction={pendingInteraction}
194
185
  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 })
186
+ const lastEvent = history[history.length - 1];
187
+ setSendingState({
188
+ slug,
189
+ targetKey,
190
+ historyLength: history.length,
191
+ lastEventTimestamp: lastEvent?.timestamp || null,
210
192
  });
211
- setTimeout(fetchData, 1000);
193
+ try {
194
+ const res = await fetch(submitUrl, {
195
+ method: "POST",
196
+ headers: { "Content-Type": "application/json" },
197
+ body: JSON.stringify({ slug, targetKey, response })
198
+ });
199
+ if (!res.ok) {
200
+ throw new Error(`Submit failed with ${res.status}`);
201
+ }
202
+ } catch (error) {
203
+ setSendingState(null);
204
+ } finally {
205
+ setTimeout(fetchData, 1000);
206
+ }
212
207
  }}
213
- disabled={status === "disconnected"}
208
+ disabled={status === "disconnected" || isSending}
214
209
  />
215
210
  </div>
216
211
  ) : (
217
- <ContentCard item={currentItem} />
212
+ <ContentCard item={currentItem} promptSearchRequestId={promptSearchRequestId} />
218
213
  )}
219
214
  </motion.div>
220
215
  </AnimatePresence>
@@ -230,6 +225,18 @@ export default function App() {
230
225
  hasNew={hasNew}
231
226
  onJumpToLatest={() => setPageIndex(history.length - 1)}
232
227
  className={viewMode === "log" ? "opacity-0 pointer-events-none" : "opacity-100"}
228
+ leftSlot={
229
+ isAgentStarted ? (
230
+ <button
231
+ type="button"
232
+ onClick={() => setPromptSearchRequestId((prev) => prev + 1)}
233
+ className="w-12 h-12 rounded-full bg-white text-black dark:bg-black dark:text-white border border-black/10 dark:border-white/10 flex items-center justify-center shadow-2xl shadow-black/20 dark:shadow-white/10 hover:scale-[1.02] transition-transform"
234
+ aria-label="Search prompt sections"
235
+ >
236
+ <Search className="w-5 h-5" />
237
+ </button>
238
+ ) : null
239
+ }
233
240
  />
234
241
  </div>
235
242
  );
@@ -1,15 +1,31 @@
1
- import { useMemo, useState } from "react";
1
+ import { useEffect, useMemo, useState } from "react";
2
2
  import { Bot, Check } from "lucide-react";
3
3
 
4
4
  export default function ChoiceInteraction({ interaction, onSubmit, disabled }) {
5
- const { prompt, question, options = [], multiSelect, allowCustom } = interaction;
5
+ const { prompt, question, options = [], multiSelect, allowCustom, fullAuto, autoSelectDelay = 20, timestamp } = interaction;
6
6
  const [selected, setSelected] = useState(multiSelect ? [] : null);
7
7
  const [customText, setCustomText] = useState("");
8
8
  const [showCustom, setShowCustom] = useState(false);
9
+ const [tick, setTick] = useState(0);
9
10
 
10
11
  const list = useMemo(() => options || [], [options]);
11
12
  const title = prompt || question || "Choose an option.";
12
13
 
14
+ // Calculate countdown based on event timestamp
15
+ const countdown = useMemo(() => {
16
+ if (!fullAuto || !timestamp) return null;
17
+ const eventTime = new Date(timestamp).getTime();
18
+ const elapsed = Math.floor((Date.now() - eventTime) / 1000);
19
+ return autoSelectDelay - elapsed;
20
+ }, [fullAuto, timestamp, autoSelectDelay, tick]);
21
+
22
+ // Tick every second to update countdown
23
+ useEffect(() => {
24
+ if (!fullAuto) return;
25
+ const timer = setInterval(() => setTick(t => t + 1), 1000);
26
+ return () => clearInterval(timer);
27
+ }, [fullAuto]);
28
+
13
29
  const handleSelect = (key) => {
14
30
  if (multiSelect) {
15
31
  setSelected((prev) => (
@@ -44,19 +60,43 @@ export default function ChoiceInteraction({ interaction, onSubmit, disabled }) {
44
60
  <div className="w-full h-full flex flex-col items-stretch overflow-hidden">
45
61
  <div className="flex-1 overflow-y-auto custom-scroll px-6 py-12 space-y-8 flex flex-col items-center">
46
62
  <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">
63
+ <div className="w-16 h-16 rounded-3xl bg-black text-white dark:bg-white dark:text-black flex items-center justify-center mx-auto shadow-2xl shadow-black/20 dark:shadow-white/10">
48
64
  <Bot className="w-8 h-8" />
49
65
  </div>
50
- <h3 className="text-4xl font-extrabold tracking-tight text-fg pt-4 text-center">Choose an option.</h3>
66
+ <h3 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-fg pt-4 text-center">Choose an option.</h3>
51
67
  </div>
52
68
 
53
- <div className="text-xl font-medium text-fg/70 text-center max-w-2xl whitespace-pre-wrap">
69
+ <div className="text-lg sm:text-xl font-medium text-fg/70 text-center max-w-2xl whitespace-pre-wrap break-words">
54
70
  {title}
55
71
  </div>
56
72
 
73
+ {fullAuto && countdown !== null && (
74
+ <div className="text-center">
75
+ <div className={`inline-flex items-center gap-2 px-4 py-2 rounded-full ${
76
+ countdown > 0
77
+ ? "bg-yellow-500 text-black animate-pulse"
78
+ : "bg-black text-white dark:bg-white dark:text-black"
79
+ }`}>
80
+ <span className="text-xl">⚡</span>
81
+ <span className="text-sm font-bold">
82
+ {countdown > 0
83
+ ? `Agent deciding in ${countdown}s...`
84
+ : "Auto-selecting recommended option..."}
85
+ </span>
86
+ </div>
87
+ </div>
88
+ )}
89
+
57
90
  <div className="w-full max-w-2xl space-y-3">
58
- {list.map((opt) => {
91
+ {list.map((opt, index) => {
59
92
  const isSelected = multiSelect ? selected.includes(opt.key) : selected === opt.key;
93
+ const labelClass = isSelected
94
+ ? "text-white dark:text-black"
95
+ : "text-black dark:text-white";
96
+ const descriptionClass = isSelected
97
+ ? "text-white/70 dark:text-black/70"
98
+ : "text-black/50 dark:text-white/50";
99
+
60
100
  return (
61
101
  <button
62
102
  key={opt.key}
@@ -65,21 +105,32 @@ export default function ChoiceInteraction({ interaction, onSubmit, disabled }) {
65
105
  type="button"
66
106
  className={`w-full p-6 rounded-2xl border-2 transition-all text-left ${
67
107
  isSelected
68
- ? "border-accent bg-accent/10"
69
- : "border-white/10 hover:border-white/20 bg-black/[0.03] dark:bg-white/[0.03]"
108
+ ? "border-black bg-black text-white dark:border-white dark:bg-white dark:text-black"
109
+ : "border-black/10 dark:border-white/10 hover:border-black/30 dark:hover:border-white/30 bg-black/[0.02] dark:bg-white/[0.03]"
70
110
  }`}
71
111
  >
72
112
  <div className="flex items-center gap-4">
73
113
  <div className={`w-6 h-6 rounded-full border-2 flex items-center justify-center ${
74
114
  isSelected
75
- ? "border-accent bg-accent text-white"
76
- : "border-white/20"
115
+ ? "border-black bg-black text-white dark:border-white dark:bg-white dark:text-black"
116
+ : "border-black/30 dark:border-white/30"
77
117
  }`}>
78
118
  {isSelected && <Check className="w-4 h-4" />}
79
119
  </div>
80
120
  <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>}
121
+ <div className={`font-bold text-lg break-words flex flex-wrap items-center gap-2 ${labelClass}`}>
122
+ <span className="break-words">{opt.label || opt.key}</span>
123
+ {index === 0 && (
124
+ <span className={`ml-2 text-xs font-medium px-2 py-0.5 rounded-full ${
125
+ isSelected
126
+ ? 'bg-white/20 dark:bg-black/20'
127
+ : 'bg-black/10 dark:bg-white/10'
128
+ }`}>
129
+ Recommended
130
+ </span>
131
+ )}
132
+ </div>
133
+ {opt.description && <div className={`text-sm mt-1 break-words ${descriptionClass}`}>{opt.description}</div>}
83
134
  </div>
84
135
  </div>
85
136
  </button>
@@ -93,12 +144,12 @@ export default function ChoiceInteraction({ interaction, onSubmit, disabled }) {
93
144
  type="button"
94
145
  className={`w-full p-6 rounded-2xl border-2 transition-all text-left ${
95
146
  showCustom
96
- ? "border-accent bg-accent/10"
97
- : "border-white/10 hover:border-white/20 bg-black/[0.03] dark:bg-white/[0.03]"
147
+ ? "border-black bg-black text-white dark:border-white dark:bg-white dark:text-black"
148
+ : "border-black/10 dark:border-white/10 hover:border-black/30 dark:hover:border-white/30 bg-black/[0.02] dark:bg-white/[0.03]"
98
149
  }`}
99
150
  >
100
- <div className="font-bold text-lg">Other</div>
101
- <div className="text-sm text-fg/50 mt-1">Provide a custom response</div>
151
+ <div className={`font-bold text-lg break-words ${showCustom ? "text-white dark:text-black" : "text-black dark:text-white"}`}>Other</div>
152
+ <div className={`text-sm mt-1 break-words ${showCustom ? "text-white/70 dark:text-black/70" : "text-black/50 dark:text-white/50"}`}>Provide a custom response</div>
102
153
  </button>
103
154
  )}
104
155
 
@@ -107,13 +158,13 @@ export default function ChoiceInteraction({ interaction, onSubmit, disabled }) {
107
158
  value={customText}
108
159
  onChange={(event) => setCustomText(event.target.value)}
109
160
  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"
161
+ className="w-full h-32 p-6 rounded-2xl bg-black/[0.02] dark:bg-white/[0.03] border-2 border-black/20 dark:border-white/20 focus:border-black dark:focus:border-white focus:outline-none text-lg"
111
162
  />
112
163
  )}
113
164
  </div>
114
165
  </div>
115
166
 
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">
167
+ <div className="p-4 flex justify-center bg-gradient-to-t from-bg via-bg to-transparent shrink-0 border-t border-black/10 dark:border-white/10">
117
168
  <button
118
169
  onClick={handleSubmit}
119
170
  disabled={disabled || !isValid}
@@ -13,35 +13,35 @@ export default function ConfirmInteraction({ interaction, onSubmit, disabled })
13
13
  <div className="w-full h-full flex flex-col items-stretch overflow-hidden">
14
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
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">
16
+ <div className="w-16 h-16 rounded-3xl bg-black text-white dark:bg-white dark:text-black flex items-center justify-center mx-auto shadow-2xl shadow-black/20 dark:shadow-white/10">
17
17
  <Bot className="w-8 h-8" />
18
18
  </div>
19
- <h3 className="text-4xl font-extrabold tracking-tight text-fg pt-4 text-center">Confirm action.</h3>
19
+ <h3 className="text-3xl sm:text-4xl font-extrabold tracking-tight text-fg pt-4 text-center">Confirm action.</h3>
20
20
  </div>
21
21
 
22
- <div className="text-xl font-medium text-fg/70 text-center max-w-2xl whitespace-pre-wrap">
22
+ <div className="text-lg sm:text-xl font-medium text-fg/70 text-center max-w-2xl whitespace-pre-wrap break-words">
23
23
  {prompt || question || "Please confirm."}
24
24
  </div>
25
25
 
26
26
  {context?.documentPath && (
27
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>
28
+ Review: <code className="bg-black/10 dark:bg-white/10 px-2 py-1 rounded">{context.documentPath}</code>
29
29
  </div>
30
30
  )}
31
31
  </div>
32
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">
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-black/10 dark:border-white/10">
34
34
  <button
35
35
  onClick={() => onSubmit({ confirmed: false, raw: cancelLabel })}
36
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"
37
+ className="px-12 py-6 border border-black/20 dark:border-white/20 text-fg rounded-full font-bold text-xl hover:bg-black/5 dark:hover:bg-white/10 transition-all disabled:opacity-30"
38
38
  >
39
39
  {cancelLabel}
40
40
  </button>
41
41
  <button
42
42
  onClick={() => onSubmit({ confirmed: true, raw: confirmLabel })}
43
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"
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 shadow-black/20 dark:shadow-white/10"
45
45
  >
46
46
  {confirmLabel}
47
47
  </button>