@chrysb/alphaclaw 0.5.5 → 0.5.7-beta.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.
Files changed (86) hide show
  1. package/bin/alphaclaw.js +6 -1
  2. package/lib/public/css/agents.css +92 -0
  3. package/lib/public/css/explorer.css +101 -0
  4. package/lib/public/css/shell.css +15 -4
  5. package/lib/public/js/app.js +69 -3
  6. package/lib/public/js/components/action-button.js +5 -0
  7. package/lib/public/js/components/agents-tab/agent-bindings-section/helpers.js +76 -0
  8. package/lib/public/js/components/agents-tab/agent-bindings-section/index.js +490 -0
  9. package/lib/public/js/components/agents-tab/agent-bindings-section/use-agent-bindings.js +256 -0
  10. package/lib/public/js/components/agents-tab/agent-detail-panel.js +74 -0
  11. package/lib/public/js/components/agents-tab/agent-identity-section.js +175 -0
  12. package/lib/public/js/components/agents-tab/agent-overview/index.js +53 -0
  13. package/lib/public/js/components/agents-tab/agent-overview/manage-card.js +44 -0
  14. package/lib/public/js/components/agents-tab/agent-overview/model-card.js +158 -0
  15. package/lib/public/js/components/agents-tab/agent-overview/use-model-card.js +169 -0
  16. package/lib/public/js/components/agents-tab/agent-overview/use-workspace-card.js +45 -0
  17. package/lib/public/js/components/agents-tab/agent-overview/workspace-card.js +47 -0
  18. package/lib/public/js/components/agents-tab/agent-pairing-section.js +265 -0
  19. package/lib/public/js/components/agents-tab/create-agent-modal.js +189 -0
  20. package/lib/public/js/components/agents-tab/create-channel-modal.js +323 -0
  21. package/lib/public/js/components/agents-tab/delete-agent-dialog.js +50 -0
  22. package/lib/public/js/components/agents-tab/edit-agent-modal.js +109 -0
  23. package/lib/public/js/components/agents-tab/index.js +148 -0
  24. package/lib/public/js/components/agents-tab/use-agents.js +89 -0
  25. package/lib/public/js/components/channel-account-status-badge.js +35 -0
  26. package/lib/public/js/components/channel-operations-panel.js +33 -0
  27. package/lib/public/js/components/channels.js +545 -60
  28. package/lib/public/js/components/envars.js +25 -4
  29. package/lib/public/js/components/general/index.js +21 -11
  30. package/lib/public/js/components/general/use-general-tab.js +78 -16
  31. package/lib/public/js/components/google/gmail-setup-wizard.js +1 -3
  32. package/lib/public/js/components/google/index.js +28 -30
  33. package/lib/public/js/components/icons.js +37 -0
  34. package/lib/public/js/components/models-tab/index.js +58 -224
  35. package/lib/public/js/components/models-tab/model-picker.js +212 -0
  36. package/lib/public/js/components/models-tab/use-models.js +17 -14
  37. package/lib/public/js/components/onboarding/use-welcome-pairing.js +4 -4
  38. package/lib/public/js/components/onboarding/welcome-pairing-step.js +2 -2
  39. package/lib/public/js/components/overflow-menu.js +122 -0
  40. package/lib/public/js/components/pairings.js +36 -8
  41. package/lib/public/js/components/routes/agents-route.js +27 -0
  42. package/lib/public/js/components/routes/general-route.js +2 -0
  43. package/lib/public/js/components/routes/index.js +1 -0
  44. package/lib/public/js/components/routes/telegram-route.js +2 -2
  45. package/lib/public/js/components/secret-input.js +8 -1
  46. package/lib/public/js/components/sidebar.js +65 -39
  47. package/lib/public/js/components/telegram-workspace/index.js +175 -74
  48. package/lib/public/js/components/telegram-workspace/manage.js +83 -10
  49. package/lib/public/js/components/telegram-workspace/onboarding.js +9 -8
  50. package/lib/public/js/components/webhooks.js +43 -18
  51. package/lib/public/js/hooks/use-app-shell-controller.js +7 -0
  52. package/lib/public/js/hooks/use-browse-navigation.js +8 -5
  53. package/lib/public/js/hooks/use-destination-session-selection.js +8 -1
  54. package/lib/public/js/lib/api.js +163 -9
  55. package/lib/public/js/lib/app-navigation.js +2 -1
  56. package/lib/public/js/lib/channel-create-operation.js +102 -0
  57. package/lib/public/js/lib/format.js +14 -0
  58. package/lib/public/js/lib/sse.js +51 -0
  59. package/lib/public/js/lib/telegram-api.js +38 -18
  60. package/lib/public/setup.html +1 -0
  61. package/lib/public/shared/browse-file-policies.json +0 -1
  62. package/lib/server/agents/service.js +1478 -0
  63. package/lib/server/constants.js +2 -2
  64. package/lib/server/env.js +3 -1
  65. package/lib/server/gateway.js +104 -20
  66. package/lib/server/gmail-serve.js +2 -12
  67. package/lib/server/gmail-watch.js +29 -2
  68. package/lib/server/onboarding/import/import-applier.js +0 -1
  69. package/lib/server/onboarding/index.js +0 -6
  70. package/lib/server/onboarding/workspace.js +74 -38
  71. package/lib/server/openclaw-config.js +23 -0
  72. package/lib/server/operation-events.js +141 -0
  73. package/lib/server/routes/agents.js +266 -0
  74. package/lib/server/routes/pairings.js +135 -25
  75. package/lib/server/routes/system.js +90 -10
  76. package/lib/server/routes/telegram.js +247 -51
  77. package/lib/server/startup.js +23 -0
  78. package/lib/server/telegram-workspace.js +61 -10
  79. package/lib/server/topic-registry.js +66 -7
  80. package/lib/server/watchdog.js +151 -27
  81. package/lib/server/webhooks.js +60 -12
  82. package/lib/server.js +40 -27
  83. package/lib/setup/core-prompts/AGENTS.md +6 -5
  84. package/lib/setup/core-prompts/TOOLS.md +1 -8
  85. package/package.json +1 -1
  86. package/lib/setup/skills/control-ui/SKILL.md +0 -62
@@ -9,6 +9,7 @@ import {
9
9
  import {
10
10
  createWebhook,
11
11
  deleteWebhook,
12
+ fetchAgents,
12
13
  fetchWebhookDetail,
13
14
  fetchWebhookRequest,
14
15
  fetchWebhookRequests,
@@ -74,6 +75,14 @@ const getRequestStatusTone = (status) => {
74
75
  };
75
76
  };
76
77
 
78
+ const formatAgentFallbackName = (agentId = "") =>
79
+ String(agentId || "")
80
+ .trim()
81
+ .split(/[-_\s]+/)
82
+ .filter(Boolean)
83
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
84
+ .join(" ") || "Main Agent";
85
+
77
86
  const jsonPretty = (value) => {
78
87
  if (typeof value === "string") {
79
88
  try {
@@ -223,24 +232,6 @@ const CreateWebhookModal = ({
223
232
  >
224
233
  </td>
225
234
  </tr>
226
- ${
227
- selectedDestination
228
- ? html`
229
- <tr class="border-t border-border">
230
- <td class="w-24 px-3 py-2 text-gray-500">Channel</td>
231
- <td class="px-3 py-2 text-gray-300 font-mono">
232
- <code>${selectedDestination.channel}</code>
233
- </td>
234
- </tr>
235
- <tr>
236
- <td class="w-24 px-3 py-2 text-gray-500">To</td>
237
- <td class="px-3 py-2 text-gray-300 font-mono">
238
- <code>${selectedDestination.to}</code>
239
- </td>
240
- </tr>
241
- `
242
- : null
243
- }
244
235
  </tbody>
245
236
  </table>
246
237
  </div>
@@ -290,6 +281,21 @@ export const Webhooks = ({
290
281
 
291
282
  const listPoll = usePolling(fetchWebhooks, 15000);
292
283
  const webhooks = listPoll.data?.webhooks || [];
284
+ const agentsPoll = usePolling(fetchAgents, 20000);
285
+ const agents = Array.isArray(agentsPoll.data?.agents)
286
+ ? agentsPoll.data.agents
287
+ : [];
288
+ const agentNameById = useMemo(
289
+ () =>
290
+ new Map(
291
+ agents.map((agent) => [
292
+ String(agent?.id || "").trim(),
293
+ String(agent?.name || "").trim() ||
294
+ formatAgentFallbackName(agent?.id),
295
+ ]),
296
+ ),
297
+ [agents],
298
+ );
293
299
 
294
300
  const detailPoll = usePolling(
295
301
  async () => {
@@ -317,6 +323,13 @@ export const Webhooks = ({
317
323
 
318
324
  const selectedWebhook = detailPoll.data;
319
325
  const selectedWebhookManaged = Boolean(selectedWebhook?.managed);
326
+ const selectedDeliveryAgentId =
327
+ String(selectedWebhook?.agentId || "main").trim() || "main";
328
+ const selectedDeliveryAgentName =
329
+ agentNameById.get(selectedDeliveryAgentId) ||
330
+ formatAgentFallbackName(selectedDeliveryAgentId);
331
+ const selectedDeliveryChannel =
332
+ String(selectedWebhook?.channel || "last").trim() || "last";
320
333
  const requests = requestsPoll.data?.requests || [];
321
334
  const webhookUrl =
322
335
  selectedWebhook?.fullUrl || `.../hooks/${selectedHookName}`;
@@ -773,6 +786,18 @@ export const Webhooks = ({
773
786
  `}
774
787
  </div>
775
788
 
789
+ <div
790
+ class="bg-black/20 border border-border rounded-lg p-3 space-y-2"
791
+ >
792
+ <p class="text-xs text-gray-500">Deliver to</p>
793
+ <p class="text-xs text-gray-200 font-mono ">
794
+ ${selectedDeliveryAgentName}${" "}
795
+ <span class="text-xs text-gray-500 font-mono">
796
+ (${selectedDeliveryChannel})</span
797
+ >
798
+ </p>
799
+ </div>
800
+
776
801
  <div
777
802
  class="bg-black/20 border border-border rounded-lg p-3 space-y-2"
778
803
  >
@@ -136,6 +136,13 @@ export const useAppShellController = ({ location = "" } = {}) => {
136
136
  window.removeEventListener("alphaclaw:browse-file-saved", handleBrowseFileSaved);
137
137
  };
138
138
  }, []);
139
+ useEffect(() => {
140
+ const handleRestartRequired = () => setRestartRequired(true);
141
+ window.addEventListener("alphaclaw:restart-required", handleRestartRequired);
142
+ return () => {
143
+ window.removeEventListener("alphaclaw:restart-required", handleRestartRequired);
144
+ };
145
+ }, []);
139
146
 
140
147
  const handleGatewayRestart = useCallback(async () => {
141
148
  if (restartingGateway) return;
@@ -11,9 +11,10 @@ export const useBrowseNavigation = ({
11
11
  setLocation = () => {},
12
12
  onCloseMobileSidebar = () => {},
13
13
  } = {}) => {
14
- const [sidebarTab, setSidebarTab] = useState(() =>
15
- location.startsWith("/browse") ? "browse" : "menu",
16
- );
14
+ const [sidebarTab, setSidebarTab] = useState(() => {
15
+ if (location.startsWith("/browse")) return "browse";
16
+ return "menu";
17
+ });
17
18
  const [lastBrowsePath, setLastBrowsePath] = useState(() => {
18
19
  const settings = readUiSettings();
19
20
  return typeof settings[kBrowseLastPathUiSettingKey] === "string"
@@ -26,7 +27,8 @@ export const useBrowseNavigation = ({
26
27
  if (
27
28
  typeof storedRoute === "string" &&
28
29
  storedRoute.startsWith("/") &&
29
- !storedRoute.startsWith("/browse")
30
+ !storedRoute.startsWith("/browse") &&
31
+ !storedRoute.startsWith("/agents")
30
32
  ) {
31
33
  return storedRoute;
32
34
  }
@@ -77,7 +79,7 @@ export const useBrowseNavigation = ({
77
79
 
78
80
  useEffect(() => {
79
81
  if (location.startsWith("/browse")) return;
80
- if (location === "/telegram") return;
82
+ if (location.startsWith("/telegram")) return;
81
83
  setLastMenuRoute((currentRoute) =>
82
84
  currentRoute === location ? currentRoute : location,
83
85
  );
@@ -150,6 +152,7 @@ export const useBrowseNavigation = ({
150
152
  }
151
153
  if (nextTab === "browse" && !location.startsWith("/browse")) {
152
154
  setLocation(buildBrowseRoute(lastBrowsePath));
155
+ return;
153
156
  }
154
157
  }, [lastBrowsePath, lastMenuRoute, location, setLocation]);
155
158
 
@@ -12,7 +12,14 @@ export const getDestinationFromSession = (sessionRow = null) => {
12
12
  const channel = String(sessionRow?.replyChannel || "").trim();
13
13
  const to = String(sessionRow?.replyTo || "").trim();
14
14
  if (!channel || !to) return null;
15
- return { channel, to };
15
+ const key = String(sessionRow?.key || "").trim();
16
+ const agentMatch = key.match(/^agent:([^:]+):/);
17
+ const agentId = String(agentMatch?.[1] || "").trim();
18
+ return {
19
+ channel,
20
+ to,
21
+ ...(agentId ? { agentId } : {}),
22
+ };
16
23
  };
17
24
 
18
25
  export const useDestinationSessionSelection = ({
@@ -1,3 +1,5 @@
1
+ import { subscribeToSse } from "./sse.js";
2
+
1
3
  const kClientTimeZoneHeader = "x-client-timezone";
2
4
 
3
5
  const getBrowserTimeZone = () => {
@@ -39,22 +41,22 @@ export async function fetchPairings() {
39
41
  return res.json();
40
42
  }
41
43
 
42
- export async function approvePairing(id, channel) {
44
+ export async function approvePairing(id, channel, accountId = "") {
43
45
  const res = await authFetch(`/api/pairings/${id}/approve`, {
44
46
  method: "POST",
45
47
  headers: { "Content-Type": "application/json" },
46
- body: JSON.stringify({ channel }),
48
+ body: JSON.stringify({ channel, accountId }),
47
49
  });
48
50
  return res.json();
49
51
  }
50
52
 
51
- export async function rejectPairing(id, channel) {
53
+ export async function rejectPairing(id, channel, accountId = "") {
52
54
  const res = await authFetch(`/api/pairings/${id}/reject`, {
53
55
  method: "POST",
54
56
  headers: { "Content-Type": "application/json" },
55
- body: JSON.stringify({ channel }),
57
+ body: JSON.stringify({ channel, accountId }),
56
58
  });
57
- return res.json();
59
+ return parseJsonOrThrow(res, "Could not reject pairing");
58
60
  }
59
61
 
60
62
  export async function fetchGoogleAccounts() {
@@ -545,8 +547,9 @@ export const setPrimaryModel = async (modelKey) => {
545
547
  return res.json();
546
548
  };
547
549
 
548
- export const fetchModelsConfig = async () => {
549
- const res = await authFetch("/api/models/config");
550
+ export const fetchModelsConfig = async ({ agentId } = {}) => {
551
+ const qs = agentId ? `?agentId=${encodeURIComponent(agentId)}` : "";
552
+ const res = await authFetch(`/api/models/config${qs}`);
550
553
  return res.json();
551
554
  };
552
555
 
@@ -555,8 +558,10 @@ export const saveModelsConfig = async ({
555
558
  configuredModels,
556
559
  profiles,
557
560
  authOrder,
558
- }) => {
559
- const res = await authFetch("/api/models/config", {
561
+ agentId,
562
+ } = {}) => {
563
+ const qs = agentId ? `?agentId=${encodeURIComponent(agentId)}` : "";
564
+ const res = await authFetch(`/api/models/config${qs}`, {
560
565
  method: "PUT",
561
566
  headers: { "Content-Type": "application/json" },
562
567
  body: JSON.stringify({ primary, configuredModels, profiles, authOrder }),
@@ -591,6 +596,155 @@ export const deleteAuthProfile = async (profileId) => {
591
596
  return res.json();
592
597
  };
593
598
 
599
+ export const fetchAgents = async () => {
600
+ const res = await authFetch("/api/agents");
601
+ return parseJsonOrThrow(res, "Could not load agents");
602
+ };
603
+
604
+ export const fetchChannelAccounts = async () => {
605
+ const res = await authFetch("/api/channels/accounts");
606
+ return parseJsonOrThrow(res, "Could not load channel accounts");
607
+ };
608
+
609
+ export const fetchChannelAccountToken = async ({
610
+ provider = "",
611
+ accountId = "default",
612
+ } = {}) => {
613
+ const params = new URLSearchParams({
614
+ provider: String(provider || ""),
615
+ accountId: String(accountId || "default"),
616
+ });
617
+ const res = await authFetch(`/api/channels/accounts/token?${params.toString()}`);
618
+ return parseJsonOrThrow(res, "Could not load channel token");
619
+ };
620
+
621
+ export const createChannelAccount = async (payload) => {
622
+ const res = await authFetch("/api/channels/accounts", {
623
+ method: "POST",
624
+ headers: { "Content-Type": "application/json" },
625
+ body: JSON.stringify(payload || {}),
626
+ });
627
+ return parseJsonOrThrow(res, "Could not create channel account");
628
+ };
629
+
630
+ export const createChannelAccountJob = async (payload) => {
631
+ const res = await authFetch("/api/channels/accounts/jobs", {
632
+ method: "POST",
633
+ headers: { "Content-Type": "application/json" },
634
+ body: JSON.stringify(payload || {}),
635
+ });
636
+ return parseJsonOrThrow(res, "Could not start channel account operation");
637
+ };
638
+
639
+ export const subscribeOperationEvents = ({
640
+ operationId = "",
641
+ onMessage = () => {},
642
+ onError = () => {},
643
+ }) =>
644
+ subscribeToSse({
645
+ url: `/api/operations/${encodeURIComponent(String(operationId || ""))}/events`,
646
+ onMessage,
647
+ onError,
648
+ });
649
+
650
+ export const updateChannelAccount = async (payload) => {
651
+ const res = await authFetch("/api/channels/accounts", {
652
+ method: "PUT",
653
+ headers: { "Content-Type": "application/json" },
654
+ body: JSON.stringify(payload || {}),
655
+ });
656
+ return parseJsonOrThrow(res, "Could not update channel account");
657
+ };
658
+
659
+ export const deleteChannelAccount = async (payload) => {
660
+ const res = await authFetch("/api/channels/accounts", {
661
+ method: "DELETE",
662
+ headers: { "Content-Type": "application/json" },
663
+ body: JSON.stringify(payload || {}),
664
+ });
665
+ return parseJsonOrThrow(res, "Could not delete channel account");
666
+ };
667
+
668
+ export const fetchAgent = async (agentId) => {
669
+ const res = await authFetch(`/api/agents/${encodeURIComponent(String(agentId || ""))}`);
670
+ return parseJsonOrThrow(res, "Could not load agent");
671
+ };
672
+
673
+ export const fetchAgentWorkspaceSize = async (agentId) => {
674
+ const res = await authFetch(
675
+ `/api/agents/${encodeURIComponent(String(agentId || ""))}/workspace-size`,
676
+ );
677
+ return parseJsonOrThrow(res, "Could not load workspace size");
678
+ };
679
+
680
+ export const fetchAgentBindings = async (agentId) => {
681
+ const res = await authFetch(
682
+ `/api/agents/${encodeURIComponent(String(agentId || ""))}/bindings`,
683
+ );
684
+ return parseJsonOrThrow(res, "Could not load agent bindings");
685
+ };
686
+
687
+ export const createAgent = async (payload) => {
688
+ const res = await authFetch("/api/agents", {
689
+ method: "POST",
690
+ headers: { "Content-Type": "application/json" },
691
+ body: JSON.stringify(payload || {}),
692
+ });
693
+ return parseJsonOrThrow(res, "Could not create agent");
694
+ };
695
+
696
+ export const updateAgent = async (agentId, payload) => {
697
+ const res = await authFetch(`/api/agents/${encodeURIComponent(String(agentId || ""))}`, {
698
+ method: "PUT",
699
+ headers: { "Content-Type": "application/json" },
700
+ body: JSON.stringify(payload || {}),
701
+ });
702
+ return parseJsonOrThrow(res, "Could not update agent");
703
+ };
704
+
705
+ export const addAgentBinding = async (agentId, payload) => {
706
+ const res = await authFetch(
707
+ `/api/agents/${encodeURIComponent(String(agentId || ""))}/bindings`,
708
+ {
709
+ method: "POST",
710
+ headers: { "Content-Type": "application/json" },
711
+ body: JSON.stringify(payload || {}),
712
+ },
713
+ );
714
+ return parseJsonOrThrow(res, "Could not add agent binding");
715
+ };
716
+
717
+ export const removeAgentBinding = async (agentId, payload) => {
718
+ const res = await authFetch(
719
+ `/api/agents/${encodeURIComponent(String(agentId || ""))}/bindings`,
720
+ {
721
+ method: "DELETE",
722
+ headers: { "Content-Type": "application/json" },
723
+ body: JSON.stringify(payload || {}),
724
+ },
725
+ );
726
+ return parseJsonOrThrow(res, "Could not remove agent binding");
727
+ };
728
+
729
+ export const deleteAgent = async (agentId, { keepWorkspace = true } = {}) => {
730
+ const query = new URLSearchParams({
731
+ keepWorkspace: keepWorkspace ? "true" : "false",
732
+ });
733
+ const res = await authFetch(
734
+ `/api/agents/${encodeURIComponent(String(agentId || ""))}?${query.toString()}`,
735
+ { method: "DELETE" },
736
+ );
737
+ return parseJsonOrThrow(res, "Could not delete agent");
738
+ };
739
+
740
+ export const setDefaultAgent = async (agentId) => {
741
+ const res = await authFetch(
742
+ `/api/agents/${encodeURIComponent(String(agentId || ""))}/default`,
743
+ { method: "POST" },
744
+ );
745
+ return parseJsonOrThrow(res, "Could not set default agent");
746
+ };
747
+
594
748
  export const fetchCodexStatus = async () => {
595
749
  const res = await authFetch("/api/codex/status");
596
750
  return res.json();
@@ -27,8 +27,9 @@ export const kNavSections = [
27
27
 
28
28
  export const getSelectedNavId = ({ isBrowseRoute = false, location = "" } = {}) => {
29
29
  if (isBrowseRoute) return "browse";
30
- if (location === "/telegram") return "";
30
+ if (location.startsWith("/telegram")) return "";
31
31
  if (location.startsWith("/models")) return "models";
32
+ if (location.startsWith("/agents")) return "agents";
32
33
  if (location.startsWith("/providers")) return "models";
33
34
  if (location.startsWith("/watchdog")) return "watchdog";
34
35
  if (location.startsWith("/usage")) return "usage";
@@ -0,0 +1,102 @@
1
+ import {
2
+ createChannelAccount,
3
+ createChannelAccountJob,
4
+ subscribeOperationEvents,
5
+ } from "./api.js";
6
+
7
+ export const createChannelAccountWithProgress = async ({
8
+ payload = {},
9
+ onPhase = () => {},
10
+ }) => {
11
+ onPhase("Loading...");
12
+ if (typeof window?.EventSource !== "function") {
13
+ return createChannelAccount(payload);
14
+ }
15
+ const startResult = await createChannelAccountJob(payload);
16
+ const operationId = String(startResult?.operationId || "").trim();
17
+ if (!operationId) {
18
+ throw new Error("Could not start channel creation operation");
19
+ }
20
+ return new Promise((resolve, reject) => {
21
+ let settleCalled = false;
22
+ let activePhase = "";
23
+ let activePhaseAtMs = 0;
24
+ let deferredPhase = null;
25
+ let deferredTimer = null;
26
+ const kPhaseMinimumVisibleMs = {
27
+ restarting: 1200,
28
+ };
29
+ const clearDeferredTimer = () => {
30
+ if (!deferredTimer) return;
31
+ clearTimeout(deferredTimer);
32
+ deferredTimer = null;
33
+ };
34
+ const applyPhase = ({ phase = "", label = "" } = {}) => {
35
+ const nextPhase = String(phase || "").trim();
36
+ const nextLabel = String(label || "").trim();
37
+ if (!nextLabel) return;
38
+ const minVisibleMs = Number(kPhaseMinimumVisibleMs[activePhase] || 0);
39
+ const elapsedMs = activePhaseAtMs > 0 ? Date.now() - activePhaseAtMs : 0;
40
+ if (
41
+ minVisibleMs > 0 &&
42
+ nextPhase &&
43
+ nextPhase !== activePhase &&
44
+ elapsedMs < minVisibleMs
45
+ ) {
46
+ deferredPhase = { phase: nextPhase, label: nextLabel };
47
+ clearDeferredTimer();
48
+ deferredTimer = setTimeout(() => {
49
+ deferredTimer = null;
50
+ const next = deferredPhase;
51
+ deferredPhase = null;
52
+ if (!next) return;
53
+ applyPhase(next);
54
+ }, minVisibleMs - elapsedMs);
55
+ return;
56
+ }
57
+ clearDeferredTimer();
58
+ deferredPhase = null;
59
+ onPhase(nextLabel);
60
+ activePhase = nextPhase;
61
+ activePhaseAtMs = Date.now();
62
+ };
63
+ const closeWithCleanup = () => {
64
+ clearDeferredTimer();
65
+ close();
66
+ };
67
+ const close = subscribeOperationEvents({
68
+ operationId,
69
+ onMessage: (entry) => {
70
+ const eventName = String(entry?.event || "").trim();
71
+ if (eventName === "phase") {
72
+ applyPhase({
73
+ phase: String(entry?.data?.phase || "").trim(),
74
+ label: String(entry?.data?.label || "").trim(),
75
+ });
76
+ return;
77
+ }
78
+ if (eventName === "done") {
79
+ if (settleCalled) return;
80
+ settleCalled = true;
81
+ closeWithCleanup();
82
+ resolve(entry?.data || {});
83
+ return;
84
+ }
85
+ if (eventName === "error") {
86
+ if (settleCalled) return;
87
+ settleCalled = true;
88
+ closeWithCleanup();
89
+ reject(
90
+ new Error(String(entry?.data?.error || "Could not create channel")),
91
+ );
92
+ }
93
+ },
94
+ onError: () => {
95
+ if (settleCalled) return;
96
+ settleCalled = true;
97
+ closeWithCleanup();
98
+ reject(new Error("Channel operation stream disconnected"));
99
+ },
100
+ });
101
+ });
102
+ };
@@ -37,6 +37,20 @@ export const formatCompactNumber = (value) => {
37
37
  return kCompactNumberFormatter.format(numberValue);
38
38
  };
39
39
 
40
+ export const formatBytes = (value) => {
41
+ const bytes = Number(value || 0);
42
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
43
+ const units = ["B", "KB", "MB", "GB", "TB"];
44
+ let unitIndex = 0;
45
+ let nextValue = bytes;
46
+ while (nextValue >= 1024 && unitIndex < units.length - 1) {
47
+ nextValue /= 1024;
48
+ unitIndex += 1;
49
+ }
50
+ const precision = nextValue >= 100 || unitIndex === 0 ? 0 : nextValue >= 10 ? 1 : 2;
51
+ return `${nextValue.toFixed(precision)} ${units[unitIndex]}`;
52
+ };
53
+
40
54
  export const formatUsd = (value) => kUsdFormatter.format(Number(value || 0));
41
55
 
42
56
  export const formatLocaleDateTime = (
@@ -0,0 +1,51 @@
1
+ const parseEventPayload = (value) => {
2
+ if (typeof value !== "string" || !value.trim()) return {};
3
+ try {
4
+ return JSON.parse(value);
5
+ } catch {
6
+ return {};
7
+ }
8
+ };
9
+
10
+ export const subscribeToSse = ({
11
+ url = "",
12
+ onMessage = () => {},
13
+ onError = () => {},
14
+ }) => {
15
+ if (typeof window?.EventSource !== "function") {
16
+ throw new Error("Server events are not supported in this browser");
17
+ }
18
+ const source = new window.EventSource(String(url || ""), { withCredentials: true });
19
+ const handlePhase = (event) => {
20
+ onMessage({
21
+ event: "phase",
22
+ data: parseEventPayload(event?.data || ""),
23
+ });
24
+ };
25
+ const handleDone = (event) => {
26
+ onMessage({
27
+ event: "done",
28
+ data: parseEventPayload(event?.data || ""),
29
+ });
30
+ };
31
+ const handleFailure = (event) => {
32
+ onMessage({
33
+ event: "error",
34
+ data: parseEventPayload(event?.data || ""),
35
+ });
36
+ };
37
+ const handleError = (event) => {
38
+ onError(event);
39
+ };
40
+ source.addEventListener("phase", handlePhase);
41
+ source.addEventListener("done", handleDone);
42
+ source.addEventListener("error", handleFailure);
43
+ source.onerror = handleError;
44
+ return () => {
45
+ source.removeEventListener("phase", handlePhase);
46
+ source.removeEventListener("done", handleDone);
47
+ source.removeEventListener("error", handleFailure);
48
+ source.onerror = null;
49
+ source.close();
50
+ };
51
+ };