agent-state-machine 2.3.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-BTLc1QSv.js"></script>
8
- <link rel="stylesheet" crossorigin href="/remote/assets/index-DLa4X08t.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">
@@ -206,7 +230,7 @@ export default function App() {
206
230
  />
207
231
  </div>
208
232
  ) : (
209
- <ContentCard item={currentItem} />
233
+ <ContentCard item={currentItem} pageIndex={pageIndex} history={history} />
210
234
  )}
211
235
  </motion.div>
212
236
  </AnimatePresence>
@@ -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
  }
@@ -1,9 +1,30 @@
1
- import { useEffect, useState } from "react";
1
+ import { useEffect, useMemo, useState } from "react";
2
2
  import CopyButton from "./CopyButton.jsx";
3
- import { Bot, Brain, ChevronRight, Search, X } from "lucide-react";
3
+ 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();
@@ -43,6 +64,38 @@ function AgentStartedIcon({ className = "" }) {
43
64
  );
44
65
  }
45
66
 
67
+ function AgentStartedPulseIcon({ className = "" }) {
68
+ return (
69
+ <div className={`relative mx-auto w-14 h-14 ${className}`} aria-hidden="true">
70
+ <span className="absolute inset-0 rounded-full border border-black/30 dark:border-white/30 animate-ping" />
71
+ <div className="absolute inset-0 rounded-full border border-black dark:border-white flex items-center justify-center bg-white dark:bg-black">
72
+ <Bot className="w-7 h-7" />
73
+ </div>
74
+ </div>
75
+ );
76
+ }
77
+
78
+ function AgentCompletedIcon({ className = "" }) {
79
+ return (
80
+ <div className={`relative mx-auto w-14 h-14 ${className}`} aria-hidden="true">
81
+ <span className="absolute inset-0 rounded-full border border-black/20 dark:border-white/20 success-ring" />
82
+ <div className="absolute inset-0 rounded-full border-2 border-black dark:border-white bg-black text-white dark:bg-white dark:text-black flex items-center justify-center checkmark-pop">
83
+ <svg
84
+ viewBox="0 0 24 24"
85
+ fill="none"
86
+ stroke="currentColor"
87
+ strokeWidth="2.4"
88
+ strokeLinecap="round"
89
+ strokeLinejoin="round"
90
+ className="w-7 h-7 checkmark-draw"
91
+ >
92
+ <path d="M5 13l4 4L19 7" />
93
+ </svg>
94
+ </div>
95
+ </div>
96
+ );
97
+ }
98
+
46
99
  function MonoBlock({ children }) {
47
100
  return (
48
101
  <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">
@@ -143,7 +196,23 @@ function renderJsonWithHighlight(value) {
143
196
  return nodes;
144
197
  }
145
198
 
146
- export default function ContentCard({ item }) {
199
+ function formatTokens(count) {
200
+ if (!count) return "0";
201
+ if (count >= 1_000_000) {
202
+ const v = (count / 1_000_000).toFixed(1).replace(/\.0$/, "");
203
+ return `${v}M`;
204
+ }
205
+ if (count >= 10_000) {
206
+ return `${Math.round(count / 1000)}k`;
207
+ }
208
+ if (count >= 1000) {
209
+ const v = (count / 1000).toFixed(1).replace(/\.0$/, "");
210
+ return `${v}k`;
211
+ }
212
+ return count.toString();
213
+ }
214
+
215
+ export default function ContentCard({ item, pageIndex = 0, history = [] }) {
147
216
  if (!item) return null;
148
217
 
149
218
  const time = new Date(item.timestamp).toLocaleTimeString([], {
@@ -155,6 +224,49 @@ export default function ContentCard({ item }) {
155
224
  const [promptQuery, setPromptQuery] = useState("");
156
225
  const [showRaw, setShowRaw] = useState(false);
157
226
  const [showPromptSearch, setShowPromptSearch] = useState(false);
227
+ const [showTokens, setShowTokens] = useState(false);
228
+ const [showTokenDetails, setShowTokenDetails] = useState(false);
229
+
230
+ // Calculate token stats: point-in-time (up to current page) and total (full session)
231
+ const tokenStats = useMemo(() => {
232
+ let pointInTime = { inputTokens: 0, outputTokens: 0 };
233
+ let total = { inputTokens: 0, outputTokens: 0, cost: 0 };
234
+ const agentBreakdown = [];
235
+
236
+ for (let i = 0; i < history.length; i++) {
237
+ const event = history[i];
238
+ if (event?.event === "AGENT_COMPLETED" && event.usage) {
239
+ const input = event.usage.inputTokens || 0;
240
+ const output = event.usage.outputTokens || 0;
241
+ const cached = event.usage.cachedTokens || 0;
242
+ const cost = event.usage.cost || 0;
243
+ total.inputTokens += input;
244
+ total.outputTokens += output;
245
+ total.cost += cost;
246
+ if (i <= pageIndex) {
247
+ pointInTime.inputTokens += input;
248
+ pointInTime.outputTokens += output;
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
+ });
260
+ }
261
+ }
262
+
263
+ return {
264
+ pointInTime,
265
+ total,
266
+ agentBreakdown,
267
+ hasTokens: total.inputTokens > 0 || total.outputTokens > 0
268
+ };
269
+ }, [history, pageIndex]);
158
270
 
159
271
  // Full-auto countdown logic (must be at top level for hooks rules)
160
272
  const isInteractionEvent = item.event === "PROMPT_REQUESTED" || item.event === "INTERACTION_REQUESTED";
@@ -507,12 +619,23 @@ export default function ContentCard({ item }) {
507
619
  content = (
508
620
  <div className="space-y-10 py-6">
509
621
  <div className="space-y-3 text-center">
510
- <AgentStartedIcon />
622
+ <AgentStartedPulseIcon />
511
623
  <div className="space-y-1">
512
624
  <div className="text-[11px] font-semibold tracking-[0.28em] uppercase text-black/50 dark:text-white/50">
513
625
  Agent started
514
626
  </div>
515
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
+ )}
516
639
  </div>
517
640
  </div>
518
641
 
@@ -543,8 +666,18 @@ export default function ContentCard({ item }) {
543
666
 
544
667
  <div className="space-y-4">
545
668
  <div className="flex flex-wrap items-center justify-between gap-3">
546
- <div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/50 dark:text-white/50">
547
- Prompt
669
+ <div className="flex items-center gap-3">
670
+ <div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/50 dark:text-white/50">
671
+ Prompt
672
+ </div>
673
+ <button
674
+ type="button"
675
+ onClick={() => setShowPromptSearch(true)}
676
+ className="p-1.5 rounded-full text-black/40 dark:text-white/40 hover:text-black dark:hover:text-white hover:bg-black/5 dark:hover:bg-white/10 transition-colors"
677
+ aria-label="Search prompt"
678
+ >
679
+ <Search className="w-4 h-4" />
680
+ </button>
548
681
  </div>
549
682
  {promptCountLabel ? (
550
683
  <div className="text-xs font-mono text-black/40 dark:text-white/40">
@@ -848,21 +981,56 @@ export default function ContentCard({ item }) {
848
981
  </div>
849
982
  );
850
983
  } else if (item.event === "AGENT_COMPLETED") {
984
+ const hasUsage = item.usage && (item.usage.inputTokens > 0 || item.usage.outputTokens > 0);
985
+
851
986
  content = (
852
987
  <div className="space-y-10 py-6">
853
988
  <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">
854
- <AgentStartedIcon className="w-12 h-12" />
989
+ <AgentCompletedIcon className="w-12 h-12" />
855
990
  <div className="text-[11px] font-semibold tracking-[0.24em] uppercase text-black/50 dark:text-white/50">
856
991
  Agent completed
857
992
  </div>
858
993
  <div className="text-2xl sm:text-3xl font-black tracking-tight">
859
994
  {item.agent || "Agent"}
860
995
  </div>
861
- {typeof item.attempts === "number" ? (
996
+
997
+ {/* Token usage display */}
998
+ {hasUsage && (
999
+ <div className="flex flex-wrap items-center justify-center gap-4 text-sm">
1000
+ <div className="flex items-center gap-2">
1001
+ <span className="text-black/50 dark:text-white/50">In:</span>
1002
+ <span className="font-mono font-semibold">{formatTokenCount(item.usage.inputTokens)}</span>
1003
+ </div>
1004
+ <div className="flex items-center gap-2">
1005
+ <span className="text-black/50 dark:text-white/50">Out:</span>
1006
+ <span className="font-mono font-semibold">{formatTokenCount(item.usage.outputTokens)}</span>
1007
+ </div>
1008
+ {item.usage.cachedTokens > 0 && (
1009
+ <div className="flex items-center gap-2 text-black/40 dark:text-white/40">
1010
+ <span>Cached:</span>
1011
+ <span className="font-mono">{formatTokenCount(item.usage.cachedTokens)}</span>
1012
+ </div>
1013
+ )}
1014
+ {item.usage.cost > 0 && (
1015
+ <div className="flex items-center gap-2 text-black/60 dark:text-white/60">
1016
+ <span className="font-mono font-semibold">${item.usage.cost.toFixed(4)}</span>
1017
+ </div>
1018
+ )}
1019
+ </div>
1020
+ )}
1021
+
1022
+ {/* Model badge */}
1023
+ {item.model && (
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">
1025
+ {formatModelName(item.model)}
1026
+ </div>
1027
+ )}
1028
+
1029
+ {typeof item.attempts === "number" && (
862
1030
  <div className="text-sm text-black/60 dark:text-white/60">
863
1031
  {item.attempts} {item.attempts === 1 ? "attempt" : "attempts"}
864
1032
  </div>
865
- ) : null}
1033
+ )}
866
1034
  </div>
867
1035
 
868
1036
  {item.output !== undefined ? (
@@ -901,16 +1069,59 @@ export default function ContentCard({ item }) {
901
1069
  <div className="content-width flex-1">
902
1070
  <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">
903
1071
  <div className="flex items-center gap-3">
904
- {item.event === "AGENT_STARTED" ? (
905
- <button
906
- type="button"
907
- onClick={() => setShowPromptSearch(true)}
908
- 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"
909
- aria-label="Search prompt sections"
910
- >
911
- <Search className="w-5 h-5" />
912
- </button>
913
- ) : null}
1072
+ {tokenStats.hasTokens && (
1073
+ <div className="relative">
1074
+ <button
1075
+ type="button"
1076
+ onClick={() => setShowTokens((prev) => !prev)}
1077
+ className="flex items-center gap-2 px-3 py-1.5 rounded-full text-xs font-mono bg-black/5 dark:bg-white/10 hover:bg-black/10 dark:hover:bg-white/15 transition-colors"
1078
+ aria-expanded={showTokens}
1079
+ aria-label="Toggle token stats"
1080
+ >
1081
+ <span className="text-black/50 dark:text-white/50">↑</span>
1082
+ <span>{formatTokens(tokenStats.pointInTime.inputTokens)}</span>
1083
+ <span className="text-black/50 dark:text-white/50">↓</span>
1084
+ <span>{formatTokens(tokenStats.pointInTime.outputTokens)}</span>
1085
+ <ChevronDown className={`w-3 h-3 text-black/40 dark:text-white/40 transition-transform ${showTokens ? "rotate-180" : ""}`} />
1086
+ </button>
1087
+ {showTokens && (
1088
+ <div className="absolute top-full left-0 mt-2 z-10 min-w-[200px] rounded-2xl border border-black/10 dark:border-white/10 bg-white dark:bg-black shadow-xl shadow-black/10 dark:shadow-white/5 p-4 space-y-3">
1089
+ <div className="space-y-1">
1090
+ <div className="text-[10px] font-semibold tracking-[0.2em] uppercase text-black/40 dark:text-white/40">
1091
+ Up to page {pageIndex + 1}
1092
+ </div>
1093
+ <div className="flex items-center gap-3 text-sm font-mono">
1094
+ <span className="text-black/50 dark:text-white/50">↑</span>
1095
+ <span className="font-semibold">{tokenStats.pointInTime.inputTokens.toLocaleString()}</span>
1096
+ <span className="text-black/50 dark:text-white/50">↓</span>
1097
+ <span className="font-semibold">{tokenStats.pointInTime.outputTokens.toLocaleString()}</span>
1098
+ </div>
1099
+ </div>
1100
+ <div className="border-t border-black/10 dark:border-white/10 pt-3 space-y-1">
1101
+ <div className="text-[10px] font-semibold tracking-[0.2em] uppercase text-black/40 dark:text-white/40">
1102
+ Session total
1103
+ </div>
1104
+ <div className="flex items-center gap-3 text-sm font-mono">
1105
+ <span className="text-black/50 dark:text-white/50">↑</span>
1106
+ <span className="font-semibold">{tokenStats.total.inputTokens.toLocaleString()}</span>
1107
+ <span className="text-black/50 dark:text-white/50">↓</span>
1108
+ <span className="font-semibold">{tokenStats.total.outputTokens.toLocaleString()}</span>
1109
+ </div>
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>
1121
+ </div>
1122
+ )}
1123
+ </div>
1124
+ )}
914
1125
  </div>
915
1126
  <div className="flex items-center gap-3">
916
1127
  <RawToggle open={showRaw} onToggle={() => setShowRaw((prev) => !prev)} />
@@ -926,6 +1137,126 @@ export default function ContentCard({ item }) {
926
1137
  content
927
1138
  )}
928
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
+ )}
929
1260
  </div>
930
1261
  );
931
1262
  }
@@ -1,7 +1,7 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import { ChevronLeft, ChevronRight } from "lucide-react";
3
3
 
4
- export default function Footer({ page, total, onNext, onPrev, onJump, hasNew, onJumpToLatest, className = "", leftSlot = null }) {
4
+ export default function Footer({ page, total, onNext, onPrev, onJump, hasNew, onJumpToLatest, className = "" }) {
5
5
  const [inputValue, setInputValue] = useState(page + 1);
6
6
  useEffect(() => setInputValue(page + 1), [page]);
7
7
 
@@ -16,11 +16,6 @@ export default function Footer({ page, total, onNext, onPrev, onJump, hasNew, on
16
16
 
17
17
  return (
18
18
  <footer className={`nav-footer transition-opacity duration-300 ${className}`}>
19
- {leftSlot ? (
20
- <div className="fixed bottom-6 left-2 sm:left-4 z-40">
21
- {leftSlot}
22
- </div>
23
- ) : null}
24
19
  <div className="footer-control">
25
20
  <button
26
21
  onClick={onPrev}
@@ -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
  );