agent-state-machine 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,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-Bnvi3AUu.js"></script>
8
- <link rel="stylesheet" crossorigin href="/remote/assets/index-DH2uv4Ll.css">
7
+ <script type="module" crossorigin src="/remote/assets/index-BSL55rdk.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/remote/assets/index-BHvHkNOe.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -6,6 +6,7 @@ import Footer from "./components/Footer.jsx";
6
6
  import Header from "./components/Header.jsx";
7
7
  import InteractionForm from "./components/InteractionForm.jsx";
8
8
  import SendingCard from "./components/SendingCard.jsx";
9
+ import SettingsModal from "./components/SettingsModal.jsx";
9
10
 
10
11
  export default function App() {
11
12
  const [history, setHistory] = useState([]);
@@ -17,6 +18,8 @@ export default function App() {
17
18
  const [pendingInteraction, setPendingInteraction] = useState(null);
18
19
  const [hasNew, setHasNew] = useState(false);
19
20
  const [sendingState, setSendingState] = useState(null);
21
+ const [config, setConfig] = useState({ fullAuto: false, autoSelectDelay: 20 });
22
+ const [settingsOpen, setSettingsOpen] = useState(false);
20
23
 
21
24
  useEffect(() => {
22
25
  const savedTheme = localStorage.getItem("rf_theme") || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
@@ -38,6 +41,7 @@ export default function App() {
38
41
  const historyUrl = token ? `/api/history/${token}` : "/api/history";
39
42
  const eventsUrl = token ? `/api/events/${token}` : "/api/events";
40
43
  const submitUrl = token ? `/api/submit/${token}` : "/api/submit";
44
+ const configUrl = token ? `/api/config/${token}` : "/api/config";
41
45
 
42
46
  const fetchData = async () => {
43
47
  try {
@@ -59,12 +63,28 @@ export default function App() {
59
63
  }
60
64
  }
61
65
  if (data.workflowName) setWorkflowName(data.workflowName);
66
+ if (data.config) setConfig(data.config);
62
67
  setStatus("connected");
63
68
  } catch (error) {
64
69
  setStatus("disconnected");
65
70
  }
66
71
  };
67
72
 
73
+ const updateConfig = async (updates) => {
74
+ try {
75
+ const res = await fetch(configUrl, {
76
+ method: "POST",
77
+ headers: { "Content-Type": "application/json" },
78
+ body: JSON.stringify(updates),
79
+ });
80
+ if (res.ok) {
81
+ setConfig((prev) => ({ ...prev, ...updates }));
82
+ }
83
+ } catch (err) {
84
+ console.error("Failed to update config:", err);
85
+ }
86
+ };
87
+
68
88
  const prevLen = useRef(0);
69
89
  useEffect(() => {
70
90
  if (history.length > prevLen.current) {
@@ -152,6 +172,10 @@ export default function App() {
152
172
  viewMode={viewMode}
153
173
  setViewMode={setViewMode}
154
174
  history={history}
175
+ fullAuto={config.fullAuto}
176
+ onToggleFullAuto={() => updateConfig({ fullAuto: !config.fullAuto })}
177
+ onOpenSettings={() => setSettingsOpen(true)}
178
+ configDisabled={status !== "connected"}
155
179
  />
156
180
 
157
181
  <main className="main-stage overflow-hidden">
@@ -223,6 +247,17 @@ export default function App() {
223
247
  onJumpToLatest={() => setPageIndex(history.length - 1)}
224
248
  className={viewMode === "log" ? "opacity-0 pointer-events-none" : "opacity-100"}
225
249
  />
250
+
251
+ <SettingsModal
252
+ isOpen={settingsOpen}
253
+ onClose={() => setSettingsOpen(false)}
254
+ fullAuto={config.fullAuto}
255
+ onToggleFullAuto={() => updateConfig({ fullAuto: !config.fullAuto })}
256
+ autoSelectDelay={config.autoSelectDelay}
257
+ onDelayChange={(delay) => updateConfig({ autoSelectDelay: delay })}
258
+ onStop={() => updateConfig({ stop: true })}
259
+ disabled={status !== "connected"}
260
+ />
226
261
  </div>
227
262
  );
228
263
  }
@@ -4,6 +4,27 @@ import { Bot, Brain, ChevronDown, ChevronRight, Search, X } from "lucide-react";
4
4
 
5
5
  const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6
6
 
7
+ // Format model string for display - extracts model ID from various formats
8
+ const formatModelName = (model) => {
9
+ if (!model) return null;
10
+ // Handle api:provider:model format (e.g., "api:google:gemini-2.5-pro" -> "gemini-2.5-pro")
11
+ if (model.startsWith("api:")) {
12
+ const parts = model.split(":");
13
+ return parts.length >= 3 ? parts.slice(2).join(":") : model;
14
+ }
15
+ // Handle CLI commands with -m/--model flag (e.g., "gemini -m gemini-2.5-flash-lite" -> "gemini-2.5-flash-lite")
16
+ if (model.includes(" ")) {
17
+ const parts = model.split(/\s+/);
18
+ const mIndex = parts.findIndex(p => p === "-m" || p === "--model");
19
+ if (mIndex !== -1 && parts[mIndex + 1]) {
20
+ return parts[mIndex + 1];
21
+ }
22
+ // No -m flag, just return the CLI name
23
+ return parts[0];
24
+ }
25
+ return model;
26
+ };
27
+
7
28
  const highlightText = (text, query) => {
8
29
  const source = String(text ?? "");
9
30
  const q = (query || "").trim();
@@ -204,29 +225,45 @@ export default function ContentCard({ item, pageIndex = 0, history = [] }) {
204
225
  const [showRaw, setShowRaw] = useState(false);
205
226
  const [showPromptSearch, setShowPromptSearch] = useState(false);
206
227
  const [showTokens, setShowTokens] = useState(false);
228
+ const [showTokenDetails, setShowTokenDetails] = useState(false);
207
229
 
208
230
  // Calculate token stats: point-in-time (up to current page) and total (full session)
209
231
  const tokenStats = useMemo(() => {
210
232
  let pointInTime = { inputTokens: 0, outputTokens: 0 };
211
- let total = { inputTokens: 0, outputTokens: 0 };
233
+ let total = { inputTokens: 0, outputTokens: 0, cost: 0 };
234
+ const agentBreakdown = [];
212
235
 
213
236
  for (let i = 0; i < history.length; i++) {
214
237
  const event = history[i];
215
238
  if (event?.event === "AGENT_COMPLETED" && event.usage) {
216
239
  const input = event.usage.inputTokens || 0;
217
240
  const output = event.usage.outputTokens || 0;
241
+ const cached = event.usage.cachedTokens || 0;
242
+ const cost = event.usage.cost || 0;
218
243
  total.inputTokens += input;
219
244
  total.outputTokens += output;
245
+ total.cost += cost;
220
246
  if (i <= pageIndex) {
221
247
  pointInTime.inputTokens += input;
222
248
  pointInTime.outputTokens += output;
223
249
  }
250
+ agentBreakdown.push({
251
+ agent: event.agent || "Unknown",
252
+ model: event.model || null,
253
+ inputTokens: input,
254
+ outputTokens: output,
255
+ cachedTokens: cached,
256
+ cost: cost,
257
+ timestamp: event.timestamp,
258
+ index: i,
259
+ });
224
260
  }
225
261
  }
226
262
 
227
263
  return {
228
264
  pointInTime,
229
265
  total,
266
+ agentBreakdown,
230
267
  hasTokens: total.inputTokens > 0 || total.outputTokens > 0
231
268
  };
232
269
  }, [history, pageIndex]);
@@ -588,6 +625,17 @@ export default function ContentCard({ item, pageIndex = 0, history = [] }) {
588
625
  Agent started
589
626
  </div>
590
627
  <h2 className="text-3xl sm:text-4xl font-black tracking-tight">{item.agent}</h2>
628
+ {item.model && (
629
+ <div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-black/5 dark:bg-white/10 text-xs font-mono text-black/60 dark:text-white/60">
630
+ {item.modelAlias && (
631
+ <>
632
+ <span className="font-semibold">{item.modelAlias}</span>
633
+ <span className="text-black/30 dark:text-white/30">•</span>
634
+ </>
635
+ )}
636
+ {formatModelName(item.model)}
637
+ </div>
638
+ )}
591
639
  </div>
592
640
  </div>
593
641
 
@@ -974,7 +1022,7 @@ export default function ContentCard({ item, pageIndex = 0, history = [] }) {
974
1022
  {/* Model badge */}
975
1023
  {item.model && (
976
1024
  <div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-black/5 dark:bg-white/10 text-xs font-mono text-black/60 dark:text-white/60">
977
- {item.model}
1025
+ {formatModelName(item.model)}
978
1026
  </div>
979
1027
  )}
980
1028
 
@@ -1060,6 +1108,16 @@ export default function ContentCard({ item, pageIndex = 0, history = [] }) {
1060
1108
  <span className="font-semibold">{tokenStats.total.outputTokens.toLocaleString()}</span>
1061
1109
  </div>
1062
1110
  </div>
1111
+ <button
1112
+ type="button"
1113
+ onClick={() => {
1114
+ setShowTokens(false);
1115
+ setShowTokenDetails(true);
1116
+ }}
1117
+ className="w-full mt-2 pt-3 border-t border-black/10 dark:border-white/10 text-[10px] font-semibold tracking-[0.16em] uppercase text-black/50 dark:text-white/50 hover:text-black dark:hover:text-white transition-colors text-center"
1118
+ >
1119
+ View details
1120
+ </button>
1063
1121
  </div>
1064
1122
  )}
1065
1123
  </div>
@@ -1079,6 +1137,126 @@ export default function ContentCard({ item, pageIndex = 0, history = [] }) {
1079
1137
  content
1080
1138
  )}
1081
1139
  </div>
1140
+
1141
+ {/* Token Details Modal */}
1142
+ {showTokenDetails && (
1143
+ <div className="fixed inset-0 z-50 bg-white text-black dark:bg-black dark:text-white">
1144
+ <div className="h-full w-full overflow-y-auto custom-scroll px-6 sm:px-10 py-10">
1145
+ <div className="max-w-4xl mx-auto space-y-8">
1146
+ <div className="flex flex-wrap items-center justify-between gap-4">
1147
+ <div className="space-y-1">
1148
+ <div className="text-[11px] font-semibold tracking-[0.32em] uppercase text-black/50 dark:text-white/50">
1149
+ Token Usage Details
1150
+ </div>
1151
+ <div className="text-2xl font-bold">
1152
+ {tokenStats.agentBreakdown.length} agent {tokenStats.agentBreakdown.length === 1 ? "call" : "calls"}
1153
+ </div>
1154
+ </div>
1155
+ <button
1156
+ type="button"
1157
+ onClick={() => setShowTokenDetails(false)}
1158
+ className="text-[10px] font-bold tracking-[0.2em] uppercase text-black/60 hover:text-black dark:text-white/60 dark:hover:text-white"
1159
+ >
1160
+ Close
1161
+ </button>
1162
+ </div>
1163
+
1164
+ {/* Totals Summary */}
1165
+ <div className="rounded-[24px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04] p-6">
1166
+ <div className="text-[10px] font-semibold tracking-[0.2em] uppercase text-black/40 dark:text-white/40 mb-4">
1167
+ Session Totals
1168
+ </div>
1169
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-6">
1170
+ <div className="space-y-1">
1171
+ <div className="text-xs text-black/50 dark:text-white/50">Input Tokens</div>
1172
+ <div className="text-xl font-mono font-bold">{tokenStats.total.inputTokens.toLocaleString()}</div>
1173
+ </div>
1174
+ <div className="space-y-1">
1175
+ <div className="text-xs text-black/50 dark:text-white/50">Output Tokens</div>
1176
+ <div className="text-xl font-mono font-bold">{tokenStats.total.outputTokens.toLocaleString()}</div>
1177
+ </div>
1178
+ <div className="space-y-1">
1179
+ <div className="text-xs text-black/50 dark:text-white/50">Total Tokens</div>
1180
+ <div className="text-xl font-mono font-bold">{(tokenStats.total.inputTokens + tokenStats.total.outputTokens).toLocaleString()}</div>
1181
+ </div>
1182
+ {tokenStats.total.cost > 0 && (
1183
+ <div className="space-y-1">
1184
+ <div className="text-xs text-black/50 dark:text-white/50">Total Cost</div>
1185
+ <div className="text-xl font-mono font-bold">${tokenStats.total.cost.toFixed(4)}</div>
1186
+ </div>
1187
+ )}
1188
+ </div>
1189
+ </div>
1190
+
1191
+ {/* Agent Breakdown */}
1192
+ <div className="space-y-4">
1193
+ <div className="text-[10px] font-semibold tracking-[0.2em] uppercase text-black/40 dark:text-white/40">
1194
+ Per-Agent Breakdown
1195
+ </div>
1196
+ <div className="space-y-3">
1197
+ {tokenStats.agentBreakdown.map((agent, idx) => (
1198
+ <div
1199
+ key={`${agent.agent}-${idx}`}
1200
+ className="rounded-[20px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04] p-5"
1201
+ >
1202
+ <div className="flex flex-wrap items-start justify-between gap-4 mb-4">
1203
+ <div className="space-y-1">
1204
+ <div className="text-lg font-bold">{agent.agent}</div>
1205
+ <div className="flex items-center gap-2">
1206
+ {agent.model && (
1207
+ <span className="inline-flex px-2 py-0.5 rounded-full bg-black/5 dark:bg-white/10 text-xs font-mono text-black/60 dark:text-white/60">
1208
+ {formatModelName(agent.model)}
1209
+ </span>
1210
+ )}
1211
+ <span className="text-xs text-black/40 dark:text-white/40">
1212
+ {new Date(agent.timestamp).toLocaleTimeString([], {
1213
+ hour: "2-digit",
1214
+ minute: "2-digit",
1215
+ second: "2-digit",
1216
+ })}
1217
+ </span>
1218
+ </div>
1219
+ </div>
1220
+ {agent.cost > 0 && (
1221
+ <div className="text-right">
1222
+ <div className="text-lg font-mono font-bold">${agent.cost.toFixed(4)}</div>
1223
+ </div>
1224
+ )}
1225
+ </div>
1226
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 text-sm">
1227
+ <div className="space-y-0.5">
1228
+ <div className="text-xs text-black/40 dark:text-white/40">Input</div>
1229
+ <div className="font-mono font-semibold">{agent.inputTokens.toLocaleString()}</div>
1230
+ </div>
1231
+ <div className="space-y-0.5">
1232
+ <div className="text-xs text-black/40 dark:text-white/40">Output</div>
1233
+ <div className="font-mono font-semibold">{agent.outputTokens.toLocaleString()}</div>
1234
+ </div>
1235
+ {agent.cachedTokens > 0 && (
1236
+ <div className="space-y-0.5">
1237
+ <div className="text-xs text-black/40 dark:text-white/40">Cached</div>
1238
+ <div className="font-mono font-semibold text-black/60 dark:text-white/60">{agent.cachedTokens.toLocaleString()}</div>
1239
+ </div>
1240
+ )}
1241
+ <div className="space-y-0.5">
1242
+ <div className="text-xs text-black/40 dark:text-white/40">Total</div>
1243
+ <div className="font-mono font-semibold">{(agent.inputTokens + agent.outputTokens).toLocaleString()}</div>
1244
+ </div>
1245
+ </div>
1246
+ </div>
1247
+ ))}
1248
+ </div>
1249
+ </div>
1250
+
1251
+ {tokenStats.agentBreakdown.length === 0 && (
1252
+ <div className="rounded-[20px] border border-black/10 dark:border-white/10 bg-black/[0.02] dark:bg-white/[0.04] p-6 text-center text-black/50 dark:text-white/50">
1253
+ No token usage data available yet.
1254
+ </div>
1255
+ )}
1256
+ </div>
1257
+ </div>
1258
+ </div>
1259
+ )}
1082
1260
  </div>
1083
1261
  );
1084
1262
  }
@@ -1,13 +1,27 @@
1
- import { Moon, Sun, LayoutList, Presentation } from "lucide-react";
1
+ import { Moon, Sun, LayoutList, Presentation, Play, Pause, Settings } from "lucide-react";
2
2
  import CopyButton from "./CopyButton.jsx";
3
3
 
4
- export default function Header({ workflowName, status, theme, toggleTheme, viewMode, setViewMode, history }) {
4
+ export default function Header({
5
+ workflowName,
6
+ status,
7
+ theme,
8
+ toggleTheme,
9
+ viewMode,
10
+ setViewMode,
11
+ history,
12
+ fullAuto,
13
+ onToggleFullAuto,
14
+ onOpenSettings,
15
+ configDisabled,
16
+ }) {
17
+ const isConnected = status === "connected";
18
+
5
19
  return (
6
20
  <header className="fixed top-0 inset-x-0 h-20 px-6 sm:px-10 lg:px-12 flex items-center justify-between z-50 bg-bg/80 backdrop-blur-3xl">
7
21
  <div className="flex items-center gap-4">
8
22
  <div
9
23
  className={`w-2.5 h-2.5 rounded-full border ${
10
- status === "connected"
24
+ isConnected
11
25
  ? "bg-black border-black dark:bg-white dark:border-white"
12
26
  : "bg-transparent border-black/30 dark:border-white/30"
13
27
  }`}
@@ -19,7 +33,37 @@ export default function Header({ workflowName, status, theme, toggleTheme, viewM
19
33
  </div>
20
34
 
21
35
  <div className="flex items-center gap-2">
36
+ {/* Play/Pause button - quick toggle for full-auto */}
37
+ <button
38
+ onClick={onToggleFullAuto}
39
+ disabled={configDisabled}
40
+ className={`tooltip w-10 h-10 flex items-center justify-center rounded-full transition-colors ${
41
+ configDisabled
42
+ ? "opacity-30 cursor-not-allowed"
43
+ : "hover:bg-black/5 dark:hover:bg-white/10"
44
+ }`}
45
+ data-tooltip={fullAuto ? "Pause auto" : "Enable auto"}
46
+ aria-label={fullAuto ? "Disable full-auto mode" : "Enable full-auto mode"}
47
+ >
48
+ {fullAuto ? (
49
+ <Pause className="w-5 h-5 opacity-60" />
50
+ ) : (
51
+ <Play className="w-5 h-5 opacity-40 hover:opacity-100" />
52
+ )}
53
+ </button>
54
+
55
+ {/* Settings button */}
56
+ <button
57
+ onClick={onOpenSettings}
58
+ className="tooltip w-10 h-10 flex items-center justify-center rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
59
+ data-tooltip="Settings"
60
+ aria-label="Open settings"
61
+ >
62
+ <Settings className="w-5 h-5 opacity-40 hover:opacity-100" />
63
+ </button>
64
+
22
65
  <CopyButton text={history || []} label="Copy full history" disabled={!history || history.length === 0} />
66
+
23
67
  <button
24
68
  onClick={() => setViewMode(viewMode === "present" ? "log" : "present")}
25
69
  className="tooltip w-10 h-10 flex items-center justify-center rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
@@ -33,14 +77,18 @@ export default function Header({ workflowName, status, theme, toggleTheme, viewM
33
77
  )}
34
78
  </button>
35
79
 
36
- <button
37
- onClick={toggleTheme}
38
- className="tooltip w-10 h-10 flex items-center justify-center rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
39
- data-tooltip={theme === "dark" ? "Light theme" : "Dark theme"}
40
- aria-label={theme === "dark" ? "Switch to Light theme" : "Switch to Dark theme"}
41
- >
42
- {theme === "dark" ? <Sun className="w-5 h-5 opacity-40 hover:opacity-100" /> : <Moon className="w-5 h-5 opacity-40 hover:opacity-100" />}
43
- </button>
80
+ <button
81
+ onClick={toggleTheme}
82
+ className="tooltip w-10 h-10 flex items-center justify-center rounded-full hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
83
+ data-tooltip={theme === "dark" ? "Light theme" : "Dark theme"}
84
+ aria-label={theme === "dark" ? "Switch to Light theme" : "Switch to Dark theme"}
85
+ >
86
+ {theme === "dark" ? (
87
+ <Sun className="w-5 h-5 opacity-40 hover:opacity-100" />
88
+ ) : (
89
+ <Moon className="w-5 h-5 opacity-40 hover:opacity-100" />
90
+ )}
91
+ </button>
44
92
  </div>
45
93
  </header>
46
94
  );
@@ -0,0 +1,130 @@
1
+ import { X, Square } from "lucide-react";
2
+ import { useState } from "react";
3
+
4
+ export default function SettingsModal({
5
+ isOpen,
6
+ onClose,
7
+ fullAuto,
8
+ onToggleFullAuto,
9
+ autoSelectDelay,
10
+ onDelayChange,
11
+ onStop,
12
+ disabled,
13
+ }) {
14
+ const [showStopConfirm, setShowStopConfirm] = useState(false);
15
+
16
+ if (!isOpen) return null;
17
+
18
+ const handleStop = () => {
19
+ if (showStopConfirm) {
20
+ onStop();
21
+ setShowStopConfirm(false);
22
+ onClose();
23
+ } else {
24
+ setShowStopConfirm(true);
25
+ }
26
+ };
27
+
28
+ const handleBackdropClick = (e) => {
29
+ if (e.target === e.currentTarget) {
30
+ setShowStopConfirm(false);
31
+ onClose();
32
+ }
33
+ };
34
+
35
+ return (
36
+ <div
37
+ className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
38
+ onClick={handleBackdropClick}
39
+ >
40
+ <div className="bg-white dark:bg-black border border-black/10 dark:border-white/10 rounded-3xl p-8 w-[90vw] max-w-md shadow-2xl">
41
+ {/* Header */}
42
+ <div className="flex justify-between items-center mb-6">
43
+ <h2 className="text-xl font-bold">Settings</h2>
44
+ <button
45
+ onClick={() => {
46
+ setShowStopConfirm(false);
47
+ onClose();
48
+ }}
49
+ className="p-2 hover:bg-black/5 dark:hover:bg-white/10 rounded-full transition-colors"
50
+ aria-label="Close settings"
51
+ >
52
+ <X className="w-5 h-5" />
53
+ </button>
54
+ </div>
55
+
56
+ {/* Full-auto toggle */}
57
+ <div className="flex items-center justify-between py-4 border-b border-black/10 dark:border-white/10">
58
+ <div>
59
+ <div className="font-semibold">Full-Auto Mode</div>
60
+ <div className="text-sm text-black/50 dark:text-white/50">
61
+ Auto-select recommended options
62
+ </div>
63
+ </div>
64
+ <button
65
+ onClick={onToggleFullAuto}
66
+ disabled={disabled}
67
+ className={`w-12 h-7 rounded-full transition-colors relative ${
68
+ fullAuto
69
+ ? "bg-black dark:bg-white"
70
+ : "bg-black/20 dark:bg-white/20"
71
+ } ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
72
+ aria-pressed={fullAuto}
73
+ aria-label="Toggle full-auto mode"
74
+ >
75
+ <div
76
+ className={`absolute top-1 w-5 h-5 rounded-full bg-white dark:bg-black transition-transform ${
77
+ fullAuto ? "left-6" : "left-1"
78
+ }`}
79
+ />
80
+ </button>
81
+ </div>
82
+
83
+ {/* Countdown delay */}
84
+ <div className="flex items-center justify-between py-4 border-b border-black/10 dark:border-white/10">
85
+ <div>
86
+ <div className="font-semibold">Countdown Delay</div>
87
+ <div className="text-sm text-black/50 dark:text-white/50">
88
+ Seconds before auto-select
89
+ </div>
90
+ </div>
91
+ <input
92
+ type="number"
93
+ value={autoSelectDelay}
94
+ onChange={(e) => {
95
+ const val = parseInt(e.target.value, 10);
96
+ if (!isNaN(val) && val >= 1 && val <= 120) {
97
+ onDelayChange(val);
98
+ }
99
+ }}
100
+ min={1}
101
+ max={120}
102
+ disabled={disabled}
103
+ className="w-20 p-2 text-center rounded-xl border border-black/20 dark:border-white/20 bg-transparent disabled:opacity-50"
104
+ />
105
+ </div>
106
+
107
+ {/* Stop button */}
108
+ <div className="mt-6">
109
+ <button
110
+ onClick={handleStop}
111
+ disabled={disabled}
112
+ className={`w-full py-4 rounded-2xl font-bold flex items-center justify-center gap-2 transition-colors ${
113
+ showStopConfirm
114
+ ? "bg-red-600 text-white hover:bg-red-700"
115
+ : "bg-red-500/10 text-red-500 hover:bg-red-500/20"
116
+ } ${disabled ? "opacity-50 cursor-not-allowed" : ""}`}
117
+ >
118
+ <Square className="w-5 h-5" />
119
+ {showStopConfirm ? "Click again to confirm" : "Stop Workflow"}
120
+ </button>
121
+ {showStopConfirm && (
122
+ <div className="text-center text-sm text-black/50 dark:text-white/50 mt-2">
123
+ This will terminate the CLI process
124
+ </div>
125
+ )}
126
+ </div>
127
+ </div>
128
+ </div>
129
+ );
130
+ }