@chrysb/alphaclaw 0.4.6-beta.4 → 0.4.6-beta.6

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 (52) hide show
  1. package/lib/public/js/app.js +158 -1073
  2. package/lib/public/js/components/envars.js +146 -29
  3. package/lib/public/js/components/features.js +1 -1
  4. package/lib/public/js/components/general/index.js +155 -0
  5. package/lib/public/js/components/icons.js +52 -0
  6. package/lib/public/js/components/info-tooltip.js +4 -7
  7. package/lib/public/js/components/models-tab/index.js +286 -0
  8. package/lib/public/js/components/models-tab/provider-auth-card.js +369 -0
  9. package/lib/public/js/components/models-tab/use-models.js +262 -0
  10. package/lib/public/js/components/models.js +1 -1
  11. package/lib/public/js/components/providers.js +1 -1
  12. package/lib/public/js/components/routes/browse-route.js +35 -0
  13. package/lib/public/js/components/routes/doctor-route.js +21 -0
  14. package/lib/public/js/components/routes/envars-route.js +11 -0
  15. package/lib/public/js/components/routes/general-route.js +45 -0
  16. package/lib/public/js/components/routes/index.js +11 -0
  17. package/lib/public/js/components/routes/models-route.js +11 -0
  18. package/lib/public/js/components/routes/providers-route.js +11 -0
  19. package/lib/public/js/components/routes/route-redirect.js +10 -0
  20. package/lib/public/js/components/routes/telegram-route.js +11 -0
  21. package/lib/public/js/components/routes/usage-route.js +15 -0
  22. package/lib/public/js/components/routes/watchdog-route.js +32 -0
  23. package/lib/public/js/components/routes/webhooks-route.js +43 -0
  24. package/lib/public/js/components/sidebar.js +2 -3
  25. package/lib/public/js/components/tooltip.js +106 -0
  26. package/lib/public/js/components/usage-tab/constants.js +1 -1
  27. package/lib/public/js/components/usage-tab/overview-section.js +124 -50
  28. package/lib/public/js/components/usage-tab/use-usage-tab.js +42 -11
  29. package/lib/public/js/components/welcome.js +1 -1
  30. package/lib/public/js/hooks/use-app-shell-controller.js +230 -0
  31. package/lib/public/js/hooks/use-app-shell-ui.js +112 -0
  32. package/lib/public/js/hooks/use-browse-navigation.js +193 -0
  33. package/lib/public/js/hooks/use-hash-location.js +32 -0
  34. package/lib/public/js/lib/api.js +35 -0
  35. package/lib/public/js/lib/app-navigation.js +39 -0
  36. package/lib/public/js/lib/browse-restart-policy.js +28 -0
  37. package/lib/public/js/lib/browse-route.js +57 -0
  38. package/lib/public/js/lib/format.js +12 -0
  39. package/lib/public/js/lib/model-config.js +1 -0
  40. package/lib/server/auth-profiles.js +291 -53
  41. package/lib/server/constants.js +24 -8
  42. package/lib/server/doctor/service.js +0 -3
  43. package/lib/server/gateway.js +50 -31
  44. package/lib/server/onboarding/index.js +2 -0
  45. package/lib/server/onboarding/validation.js +2 -2
  46. package/lib/server/routes/models.js +214 -2
  47. package/lib/server/routes/onboarding.js +2 -0
  48. package/lib/server/routes/system.js +42 -1
  49. package/lib/server/watchdog.js +14 -1
  50. package/lib/server.js +6 -0
  51. package/lib/setup/env.template +1 -0
  52. package/package.json +1 -1
@@ -0,0 +1,193 @@
1
+ import { useState, useEffect, useRef, useCallback } from "https://esm.sh/preact/hooks";
2
+ import { readUiSettings, writeUiSettings } from "../lib/ui-settings.js";
3
+ import { kDefaultUiTab, getSelectedNavId, kNavSections } from "../lib/app-navigation.js";
4
+ import { buildBrowseRoute, normalizeBrowsePath, parseBrowseRoute } from "../lib/browse-route.js";
5
+
6
+ const kBrowseLastPathUiSettingKey = "browseLastPath";
7
+ const kLastMenuRouteUiSettingKey = "lastMenuRoute";
8
+
9
+ export const useBrowseNavigation = ({
10
+ location = "",
11
+ setLocation = () => {},
12
+ onCloseMobileSidebar = () => {},
13
+ } = {}) => {
14
+ const [sidebarTab, setSidebarTab] = useState(() =>
15
+ location.startsWith("/browse") ? "browse" : "menu",
16
+ );
17
+ const [lastBrowsePath, setLastBrowsePath] = useState(() => {
18
+ const settings = readUiSettings();
19
+ return typeof settings[kBrowseLastPathUiSettingKey] === "string"
20
+ ? settings[kBrowseLastPathUiSettingKey]
21
+ : "";
22
+ });
23
+ const [lastMenuRoute, setLastMenuRoute] = useState(() => {
24
+ const settings = readUiSettings();
25
+ const storedRoute = settings[kLastMenuRouteUiSettingKey];
26
+ if (
27
+ typeof storedRoute === "string" &&
28
+ storedRoute.startsWith("/") &&
29
+ !storedRoute.startsWith("/browse")
30
+ ) {
31
+ return storedRoute;
32
+ }
33
+ return `/${kDefaultUiTab}`;
34
+ });
35
+ const [browsePreviewPath, setBrowsePreviewPath] = useState("");
36
+ const routeHistoryRef = useRef([]);
37
+
38
+ const {
39
+ activeBrowsePath,
40
+ browseLineEndTarget,
41
+ browseLineTarget,
42
+ browseViewerMode,
43
+ isBrowseRoute,
44
+ selectedBrowsePath,
45
+ } = parseBrowseRoute({
46
+ location,
47
+ browsePreviewPath,
48
+ });
49
+
50
+ const selectedNavId = getSelectedNavId({
51
+ isBrowseRoute,
52
+ location,
53
+ });
54
+
55
+ useEffect(() => {
56
+ setSidebarTab((currentTab) => {
57
+ if (location.startsWith("/browse")) return "browse";
58
+ if (currentTab === "browse") return "menu";
59
+ return currentTab;
60
+ });
61
+ }, [location]);
62
+
63
+ useEffect(() => {
64
+ if (location.startsWith("/browse")) return;
65
+ setBrowsePreviewPath("");
66
+ }, [location]);
67
+
68
+ useEffect(() => {
69
+ const historyStack = routeHistoryRef.current;
70
+ const lastEntry = historyStack[historyStack.length - 1];
71
+ if (lastEntry === location) return;
72
+ historyStack.push(location);
73
+ if (historyStack.length > 100) {
74
+ historyStack.shift();
75
+ }
76
+ }, [location]);
77
+
78
+ useEffect(() => {
79
+ if (location.startsWith("/browse")) return;
80
+ if (location === "/telegram") return;
81
+ setLastMenuRoute((currentRoute) =>
82
+ currentRoute === location ? currentRoute : location,
83
+ );
84
+ }, [location]);
85
+
86
+ useEffect(() => {
87
+ if (!isBrowseRoute) return;
88
+ if (!selectedBrowsePath) return;
89
+ setLastBrowsePath((currentPath) =>
90
+ currentPath === selectedBrowsePath ? currentPath : selectedBrowsePath,
91
+ );
92
+ }, [isBrowseRoute, selectedBrowsePath]);
93
+
94
+ useEffect(() => {
95
+ const handleBrowseGitSynced = () => {
96
+ if (!isBrowseRoute || browseViewerMode !== "diff") return;
97
+ const activePath = String(selectedBrowsePath || "").trim();
98
+ if (!activePath) return;
99
+ setLocation(buildBrowseRoute(activePath, { view: "edit" }));
100
+ };
101
+ window.addEventListener("alphaclaw:browse-git-synced", handleBrowseGitSynced);
102
+ return () => {
103
+ window.removeEventListener("alphaclaw:browse-git-synced", handleBrowseGitSynced);
104
+ };
105
+ }, [browseViewerMode, isBrowseRoute, selectedBrowsePath, setLocation]);
106
+
107
+ useEffect(() => {
108
+ const settings = readUiSettings();
109
+ settings[kBrowseLastPathUiSettingKey] = lastBrowsePath;
110
+ settings[kLastMenuRouteUiSettingKey] = lastMenuRoute;
111
+ writeUiSettings(settings);
112
+ }, [lastBrowsePath, lastMenuRoute]);
113
+
114
+ const navigateToSubScreen = useCallback((screen) => {
115
+ setLocation(`/${screen}`);
116
+ onCloseMobileSidebar();
117
+ }, [onCloseMobileSidebar, setLocation]);
118
+
119
+ const handleBrowsePreviewFile = useCallback((nextPreviewPath) => {
120
+ const normalizedPreviewPath = normalizeBrowsePath(nextPreviewPath);
121
+ setBrowsePreviewPath(normalizedPreviewPath);
122
+ }, []);
123
+
124
+ const navigateToBrowseFile = useCallback((relativePath, options = {}) => {
125
+ const normalizedTargetPath = normalizeBrowsePath(relativePath);
126
+ const selectingDirectory =
127
+ !!options.directory || String(relativePath || "").trim().endsWith("/");
128
+ const shouldPreservePreview = selectingDirectory && !!options.preservePreview;
129
+ const activePath = normalizeBrowsePath(
130
+ browsePreviewPath || selectedBrowsePath || "",
131
+ );
132
+ const nextPreviewPath =
133
+ shouldPreservePreview && activePath && activePath !== normalizedTargetPath
134
+ ? activePath
135
+ : "";
136
+ setBrowsePreviewPath(nextPreviewPath);
137
+ const routeOptions = selectingDirectory
138
+ ? { ...options, view: "edit" }
139
+ : options;
140
+ setLocation(buildBrowseRoute(normalizedTargetPath, routeOptions));
141
+ onCloseMobileSidebar();
142
+ }, [browsePreviewPath, onCloseMobileSidebar, selectedBrowsePath, setLocation]);
143
+
144
+ const handleSelectSidebarTab = useCallback((nextTab) => {
145
+ setSidebarTab(nextTab);
146
+ if (nextTab === "menu" && location.startsWith("/browse")) {
147
+ setBrowsePreviewPath("");
148
+ setLocation(lastMenuRoute || `/${kDefaultUiTab}`);
149
+ return;
150
+ }
151
+ if (nextTab === "browse" && !location.startsWith("/browse")) {
152
+ setLocation(buildBrowseRoute(lastBrowsePath));
153
+ }
154
+ }, [lastBrowsePath, lastMenuRoute, location, setLocation]);
155
+
156
+ const handleSelectNavItem = useCallback((itemId) => {
157
+ setLocation(`/${itemId}`);
158
+ onCloseMobileSidebar();
159
+ }, [onCloseMobileSidebar, setLocation]);
160
+
161
+ const exitSubScreen = useCallback(() => {
162
+ setLocation(`/${kDefaultUiTab}`);
163
+ onCloseMobileSidebar();
164
+ }, [onCloseMobileSidebar, setLocation]);
165
+
166
+ return {
167
+ state: {
168
+ activeBrowsePath,
169
+ browseLineEndTarget,
170
+ browseLineTarget,
171
+ browsePreviewPath,
172
+ browseViewerMode,
173
+ isBrowseRoute,
174
+ routeHistoryRef,
175
+ selectedBrowsePath,
176
+ selectedNavId,
177
+ sidebarTab,
178
+ },
179
+ actions: {
180
+ buildBrowseRoute,
181
+ clearBrowsePreview: () => setBrowsePreviewPath(""),
182
+ exitSubScreen,
183
+ handleBrowsePreviewFile,
184
+ handleSelectNavItem,
185
+ handleSelectSidebarTab,
186
+ navigateToBrowseFile,
187
+ navigateToSubScreen,
188
+ },
189
+ constants: {
190
+ kNavSections,
191
+ },
192
+ };
193
+ };
@@ -0,0 +1,32 @@
1
+ import { useState, useEffect, useCallback } from "https://esm.sh/preact/hooks";
2
+ import { kDefaultUiTab } from "../lib/app-navigation.js";
3
+
4
+ const getHashPath = () => {
5
+ const hash = window.location.hash.replace(/^#/, "");
6
+ if (!hash) return `/${kDefaultUiTab}`;
7
+ return hash.startsWith("/") ? hash : `/${hash}`;
8
+ };
9
+
10
+ export const useHashLocation = () => {
11
+ const [location, setLocationState] = useState(getHashPath);
12
+
13
+ useEffect(() => {
14
+ const onHashChange = () => setLocationState(getHashPath());
15
+ window.addEventListener("hashchange", onHashChange);
16
+ return () => window.removeEventListener("hashchange", onHashChange);
17
+ }, []);
18
+
19
+ const setLocation = useCallback((to) => {
20
+ const normalized = to.startsWith("/") ? to : `/${to}`;
21
+ const nextHash = `#${normalized}`;
22
+ if (window.location.hash !== nextHash) {
23
+ window.location.hash = normalized;
24
+ return;
25
+ }
26
+ setLocationState(normalized);
27
+ }, []);
28
+
29
+ return [location, setLocation];
30
+ };
31
+
32
+ export const getHashRouterPath = getHashPath;
@@ -496,6 +496,41 @@ export const setPrimaryModel = async (modelKey) => {
496
496
  return res.json();
497
497
  };
498
498
 
499
+ export const fetchModelsConfig = async () => {
500
+ const res = await authFetch('/api/models/config');
501
+ return res.json();
502
+ };
503
+
504
+ export const saveModelsConfig = async ({ primary, configuredModels, profiles, authOrder }) => {
505
+ const res = await authFetch('/api/models/config', {
506
+ method: 'PUT',
507
+ headers: { 'Content-Type': 'application/json' },
508
+ body: JSON.stringify({ primary, configuredModels, profiles, authOrder }),
509
+ });
510
+ return res.json();
511
+ };
512
+
513
+ export const fetchAuthProfiles = async () => {
514
+ const res = await authFetch('/api/models/auth');
515
+ return res.json();
516
+ };
517
+
518
+ export const upsertAuthProfile = async (profileId, credential) => {
519
+ const res = await authFetch(`/api/models/auth/${encodeURIComponent(profileId)}`, {
520
+ method: 'PUT',
521
+ headers: { 'Content-Type': 'application/json' },
522
+ body: JSON.stringify(credential),
523
+ });
524
+ return res.json();
525
+ };
526
+
527
+ export const deleteAuthProfile = async (profileId) => {
528
+ const res = await authFetch(`/api/models/auth/${encodeURIComponent(profileId)}`, {
529
+ method: 'DELETE',
530
+ });
531
+ return res.json();
532
+ };
533
+
499
534
  export const fetchCodexStatus = async () => {
500
535
  const res = await authFetch('/api/codex/status');
501
536
  return res.json();
@@ -0,0 +1,39 @@
1
+ export const kDefaultUiTab = "general";
2
+
3
+ export const kNavSections = [
4
+ {
5
+ label: "Setup",
6
+ items: [
7
+ { id: "general", label: "General" },
8
+ ],
9
+ },
10
+ {
11
+ label: "Monitoring",
12
+ items: [
13
+ { id: "watchdog", label: "Watchdog" },
14
+ { id: "usage", label: "Usage" },
15
+ { id: "doctor", label: "Doctor" },
16
+ ],
17
+ },
18
+ {
19
+ label: "Config",
20
+ items: [
21
+ { id: "models", label: "Models" },
22
+ { id: "envars", label: "Envars" },
23
+ { id: "webhooks", label: "Webhooks" },
24
+ ],
25
+ },
26
+ ];
27
+
28
+ export const getSelectedNavId = ({ isBrowseRoute = false, location = "" } = {}) => {
29
+ if (isBrowseRoute) return "browse";
30
+ if (location === "/telegram") return "";
31
+ if (location.startsWith("/models")) return "models";
32
+ if (location.startsWith("/providers")) return "models";
33
+ if (location.startsWith("/watchdog")) return "watchdog";
34
+ if (location.startsWith("/usage")) return "usage";
35
+ if (location.startsWith("/doctor")) return "doctor";
36
+ if (location.startsWith("/envars")) return "envars";
37
+ if (location.startsWith("/webhooks")) return "webhooks";
38
+ return kDefaultUiTab;
39
+ };
@@ -0,0 +1,28 @@
1
+ const kBrowseRestartRequiredRules = [
2
+ { type: "file", path: "openclaw.json" },
3
+ { type: "directory", path: "hooks/transforms" },
4
+ ];
5
+
6
+ const normalizeRestartRulePath = (value) =>
7
+ String(value || "")
8
+ .trim()
9
+ .replace(/^\/+|\/+$/g, "");
10
+
11
+ const matchesBrowseRestartRequiredRule = (path, rule) => {
12
+ const normalizedPath = normalizeRestartRulePath(path);
13
+ if (!normalizedPath) return false;
14
+ if (!rule || typeof rule !== "object") return false;
15
+ const type = String(rule.type || "").toLowerCase();
16
+ const targetPath = normalizeRestartRulePath(rule.path);
17
+ if (!targetPath) return false;
18
+ if (type === "directory") {
19
+ return normalizedPath === targetPath || normalizedPath.startsWith(`${targetPath}/`);
20
+ }
21
+ if (type === "file") {
22
+ return normalizedPath === targetPath;
23
+ }
24
+ return false;
25
+ };
26
+
27
+ export const shouldRequireRestartForBrowsePath = (path) =>
28
+ kBrowseRestartRequiredRules.some((rule) => matchesBrowseRestartRequiredRule(path, rule));
@@ -0,0 +1,57 @@
1
+ export const normalizeBrowsePath = (value) => String(value || "").replace(/^\/+|\/+$/g, "");
2
+
3
+ export const buildBrowseRoute = (relativePath, options = {}) => {
4
+ const view = String(options?.view || "edit");
5
+ const encodedPath = String(relativePath || "")
6
+ .split("/")
7
+ .filter(Boolean)
8
+ .map((segment) => encodeURIComponent(segment))
9
+ .join("/");
10
+ const baseRoute = encodedPath ? `/browse/${encodedPath}` : "/browse";
11
+ const params = new URLSearchParams();
12
+ if (view === "diff" && encodedPath) params.set("view", "diff");
13
+ if (options.line) params.set("line", String(options.line));
14
+ if (options.lineEnd) params.set("lineEnd", String(options.lineEnd));
15
+ const query = params.toString();
16
+ return query ? `${baseRoute}?${query}` : baseRoute;
17
+ };
18
+
19
+ export const parseBrowseRoute = ({ location = "", browsePreviewPath = "" } = {}) => {
20
+ const isBrowseRoute = location.startsWith("/browse");
21
+ const browseRoutePath = isBrowseRoute ? String(location || "").split("?")[0] : "";
22
+ const browseRouteQuery =
23
+ isBrowseRoute && String(location || "").includes("?")
24
+ ? String(location || "").split("?").slice(1).join("?")
25
+ : "";
26
+ const selectedBrowsePath = isBrowseRoute
27
+ ? browseRoutePath
28
+ .replace(/^\/browse\/?/, "")
29
+ .split("/")
30
+ .filter(Boolean)
31
+ .map((segment) => {
32
+ try {
33
+ return decodeURIComponent(segment);
34
+ } catch {
35
+ return segment;
36
+ }
37
+ })
38
+ .join("/")
39
+ : "";
40
+ const activeBrowsePath = browsePreviewPath || selectedBrowsePath;
41
+ const browseQueryParams = isBrowseRoute ? new URLSearchParams(browseRouteQuery) : null;
42
+ const browseViewerMode =
43
+ !browsePreviewPath && browseQueryParams?.get("view") === "diff"
44
+ ? "diff"
45
+ : "edit";
46
+ const browseLineTarget = Number.parseInt(browseQueryParams?.get("line") || "", 10) || 0;
47
+ const browseLineEndTarget = Number.parseInt(browseQueryParams?.get("lineEnd") || "", 10) || 0;
48
+
49
+ return {
50
+ activeBrowsePath,
51
+ browseLineEndTarget,
52
+ browseLineTarget,
53
+ browseViewerMode,
54
+ isBrowseRoute,
55
+ selectedBrowsePath,
56
+ };
57
+ };
@@ -1,4 +1,9 @@
1
1
  const kIntegerFormatter = new Intl.NumberFormat("en-US");
2
+ const kCompactNumberFormatter = new Intl.NumberFormat("en-US", {
3
+ notation: "compact",
4
+ minimumFractionDigits: 1,
5
+ maximumFractionDigits: 1,
6
+ });
2
7
  const kUsdFormatter = new Intl.NumberFormat("en-US", {
3
8
  style: "currency",
4
9
  currency: "USD",
@@ -25,6 +30,13 @@ const isSameDay = (left, right) =>
25
30
  export const formatInteger = (value) =>
26
31
  kIntegerFormatter.format(Number(value || 0));
27
32
 
33
+ export const formatCompactNumber = (value) => {
34
+ const numberValue = Number(value || 0);
35
+ if (!Number.isFinite(numberValue)) return "0";
36
+ if (Math.abs(numberValue) < 1000) return formatInteger(numberValue);
37
+ return kCompactNumberFormatter.format(numberValue);
38
+ };
39
+
28
40
  export const formatUsd = (value) => kUsdFormatter.format(Number(value || 0));
29
41
 
30
42
  export const formatLocaleDateTime = (
@@ -158,6 +158,7 @@ export const kFeatureDefs = [
158
158
  ];
159
159
 
160
160
  export const getVisibleAiFieldKeys = (provider) => {
161
+ if (provider === "openai-codex") return new Set();
161
162
  const authProvider = getAuthProviderFromModelProvider(provider);
162
163
  const fields = kProviderAuthFields[authProvider] || [];
163
164
  return new Set(fields.map((field) => field.key));