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
@@ -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-CbgeVnKw.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);
@@ -110,8 +140,10 @@ export default function App() {
110
140
  }, [history.length]);
111
141
 
112
142
  const currentItem = history[pageIndex];
143
+ const isAgentStarted = currentItem?.event === "AGENT_STARTED";
113
144
  const isRequestEvent = currentItem && (currentItem.event === "INTERACTION_REQUESTED" || currentItem.event === "PROMPT_REQUESTED");
114
145
  const isRequest = pendingInteraction && isRequestEvent && currentItem.slug === pendingInteraction.slug;
146
+ const isSending = Boolean(sendingState);
115
147
 
116
148
  return (
117
149
  <div className="w-full h-[100dvh] flex flex-col relative overflow-hidden bg-bg">
@@ -144,34 +176,40 @@ export default function App() {
144
176
  transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
145
177
  className="w-full h-full"
146
178
  >
147
- {isRequest ? (
179
+ {isSending ? (
180
+ <SendingCard submission={sendingState} />
181
+ ) : isRequest ? (
148
182
  <div className="content-width h-full">
149
183
  <InteractionForm
150
184
  interaction={pendingInteraction}
151
185
  onSubmit={async (slug, targetKey, response) => {
152
- const responsePreview = typeof response === "string" ? response : JSON.stringify(response);
153
- setHistory((prev) => [
154
- ...prev,
155
- {
156
- timestamp: new Date().toISOString(),
157
- event: "INTERACTION_SUBMITTED",
158
- answer: responsePreview,
159
- response
160
- }
161
- ]);
162
- setPageIndex((prev) => prev + 1);
163
- await fetch(submitUrl, {
164
- method: "POST",
165
- headers: { "Content-Type": "application/json" },
166
- 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,
167
192
  });
168
- 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
+ }
169
207
  }}
170
- disabled={status === "disconnected"}
208
+ disabled={status === "disconnected" || isSending}
171
209
  />
172
210
  </div>
173
211
  ) : (
174
- <ContentCard item={currentItem} />
212
+ <ContentCard item={currentItem} promptSearchRequestId={promptSearchRequestId} />
175
213
  )}
176
214
  </motion.div>
177
215
  </AnimatePresence>
@@ -187,6 +225,18 @@ export default function App() {
187
225
  hasNew={hasNew}
188
226
  onJumpToLatest={() => setPageIndex(history.length - 1)}
189
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
+ }
190
240
  />
191
241
  </div>
192
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>