@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,262 @@
1
+ import { useState, useEffect, useRef, useCallback } from "https://esm.sh/preact/hooks";
2
+ import {
3
+ fetchModels,
4
+ fetchModelsConfig,
5
+ saveModelsConfig,
6
+ fetchCodexStatus,
7
+ disconnectCodex,
8
+ } from "../../lib/api.js";
9
+ import { showToast } from "../toast.js";
10
+
11
+ let kModelsTabCache = null;
12
+
13
+ export const useModels = () => {
14
+ const [catalog, setCatalog] = useState(() => kModelsTabCache?.catalog || []);
15
+ const [primary, setPrimary] = useState(() => kModelsTabCache?.primary || "");
16
+ const [configuredModels, setConfiguredModels] = useState(
17
+ () => kModelsTabCache?.configuredModels || {},
18
+ );
19
+ const [authProfiles, setAuthProfiles] = useState(
20
+ () => kModelsTabCache?.authProfiles || [],
21
+ );
22
+ const [authOrder, setAuthOrder] = useState(
23
+ () => kModelsTabCache?.authOrder || {},
24
+ );
25
+ const [codexStatus, setCodexStatus] = useState(
26
+ () => kModelsTabCache?.codexStatus || { connected: false },
27
+ );
28
+ const [loading, setLoading] = useState(() => !kModelsTabCache);
29
+ const [saving, setSaving] = useState(false);
30
+ const [ready, setReady] = useState(() => !!kModelsTabCache);
31
+ const [error, setError] = useState("");
32
+
33
+ const [profileEdits, setProfileEdits] = useState({});
34
+ const [orderEdits, setOrderEdits] = useState({});
35
+
36
+ const savedPrimaryRef = useRef(kModelsTabCache?.primary || "");
37
+ const savedConfiguredRef = useRef(kModelsTabCache?.configuredModels || {});
38
+
39
+ const updateCache = useCallback((patch) => {
40
+ kModelsTabCache = { ...(kModelsTabCache || {}), ...patch };
41
+ }, []);
42
+
43
+ const refresh = useCallback(async () => {
44
+ if (!ready) setLoading(true);
45
+ setError("");
46
+ try {
47
+ const [catalogResult, configResult, codex] = await Promise.all([
48
+ fetchModels(),
49
+ fetchModelsConfig(),
50
+ fetchCodexStatus(),
51
+ ]);
52
+ const catalogModels = Array.isArray(catalogResult.models)
53
+ ? catalogResult.models
54
+ : [];
55
+ setCatalog(catalogModels);
56
+ const p = configResult.primary || "";
57
+ const cm = configResult.configuredModels || {};
58
+ const ap = configResult.authProfiles || [];
59
+ const ao = configResult.authOrder || {};
60
+ setPrimary(p);
61
+ setConfiguredModels(cm);
62
+ setAuthProfiles(ap);
63
+ setAuthOrder(ao);
64
+ setCodexStatus(codex || { connected: false });
65
+ setProfileEdits({});
66
+ setOrderEdits({});
67
+ savedPrimaryRef.current = p;
68
+ savedConfiguredRef.current = cm;
69
+ updateCache({
70
+ catalog: catalogModels,
71
+ primary: p,
72
+ configuredModels: cm,
73
+ authProfiles: ap,
74
+ authOrder: ao,
75
+ codexStatus: codex || { connected: false },
76
+ });
77
+ if (!catalogModels.length) setError("No models found");
78
+ } catch (err) {
79
+ setError("Failed to load model settings");
80
+ showToast(`Failed to load model settings: ${err.message}`, "error");
81
+ } finally {
82
+ setReady(true);
83
+ setLoading(false);
84
+ }
85
+ }, [ready, updateCache]);
86
+
87
+ useEffect(() => {
88
+ refresh();
89
+ }, []);
90
+
91
+ const modelConfigDirty =
92
+ primary !== savedPrimaryRef.current ||
93
+ JSON.stringify(configuredModels) !==
94
+ JSON.stringify(savedConfiguredRef.current);
95
+
96
+ const authDirty = (() => {
97
+ const hasProfileChanges = Object.entries(profileEdits).some(
98
+ ([id, cred]) => {
99
+ const existing = authProfiles.find((p) => p.id === id);
100
+ const newVal = cred?.key || cred?.token || cred?.access || "";
101
+ const oldVal =
102
+ existing?.key || existing?.token || existing?.access || "";
103
+ return newVal !== oldVal && newVal !== "";
104
+ },
105
+ );
106
+ const hasOrderChanges = Object.entries(orderEdits).some(
107
+ ([provider, order]) => {
108
+ const existing = authOrder[provider];
109
+ return JSON.stringify(order) !== JSON.stringify(existing);
110
+ },
111
+ );
112
+ return hasProfileChanges || hasOrderChanges;
113
+ })();
114
+
115
+ const isDirty = modelConfigDirty || authDirty;
116
+
117
+ const addModel = useCallback(
118
+ (modelKey) => {
119
+ if (!modelKey) return;
120
+ setConfiguredModels((prev) => {
121
+ const next = { ...prev, [modelKey]: {} };
122
+ updateCache({ configuredModels: next });
123
+ return next;
124
+ });
125
+ },
126
+ [updateCache],
127
+ );
128
+
129
+ const removeModel = useCallback(
130
+ (modelKey) => {
131
+ setConfiguredModels((prev) => {
132
+ const next = { ...prev };
133
+ delete next[modelKey];
134
+ updateCache({ configuredModels: next });
135
+ return next;
136
+ });
137
+ if (primary === modelKey) {
138
+ const remaining = Object.keys(configuredModels).filter(
139
+ (k) => k !== modelKey,
140
+ );
141
+ const newPrimary = remaining[0] || "";
142
+ setPrimary(newPrimary);
143
+ updateCache({ primary: newPrimary });
144
+ }
145
+ },
146
+ [primary, configuredModels, updateCache],
147
+ );
148
+
149
+ const setPrimaryModel = useCallback(
150
+ (modelKey) => {
151
+ setPrimary(modelKey);
152
+ updateCache({ primary: modelKey });
153
+ },
154
+ [updateCache],
155
+ );
156
+
157
+ const editProfile = useCallback((profileId, credential) => {
158
+ setProfileEdits((prev) => ({ ...prev, [profileId]: credential }));
159
+ }, []);
160
+
161
+ const editAuthOrder = useCallback((provider, orderedIds) => {
162
+ setOrderEdits((prev) => ({ ...prev, [provider]: orderedIds }));
163
+ }, []);
164
+
165
+ const getProfileValue = useCallback(
166
+ (profileId) => {
167
+ if (profileEdits[profileId] !== undefined) return profileEdits[profileId];
168
+ const existing = authProfiles.find((p) => p.id === profileId);
169
+ return existing || null;
170
+ },
171
+ [profileEdits, authProfiles],
172
+ );
173
+
174
+ const getEffectiveOrder = useCallback(
175
+ (provider) => {
176
+ if (orderEdits[provider] !== undefined) return orderEdits[provider];
177
+ return authOrder[provider] || null;
178
+ },
179
+ [orderEdits, authOrder],
180
+ );
181
+
182
+ const cancelChanges = useCallback(() => {
183
+ const savedPrimary = savedPrimaryRef.current || "";
184
+ const savedConfigured = savedConfiguredRef.current || {};
185
+ setPrimary(savedPrimary);
186
+ setConfiguredModels(savedConfigured);
187
+ setProfileEdits({});
188
+ setOrderEdits({});
189
+ updateCache({
190
+ primary: savedPrimary,
191
+ configuredModels: savedConfigured,
192
+ });
193
+ }, [updateCache]);
194
+
195
+ const saveAll = useCallback(async () => {
196
+ if (saving) return;
197
+ setSaving(true);
198
+ try {
199
+ const changedProfiles = Object.entries(profileEdits)
200
+ .filter(([, cred]) => {
201
+ const val = cred?.key || cred?.token || cred?.access || "";
202
+ return val !== "";
203
+ })
204
+ .map(([id, cred]) => ({ id, ...cred }));
205
+
206
+ const result = await saveModelsConfig({
207
+ primary,
208
+ configuredModels,
209
+ profiles: changedProfiles.length > 0 ? changedProfiles : undefined,
210
+ authOrder:
211
+ Object.keys(orderEdits).length > 0 ? orderEdits : undefined,
212
+ });
213
+ if (!result.ok)
214
+ throw new Error(result.error || "Failed to save config");
215
+ showToast("Changes saved", "success");
216
+ if (result.syncWarning) {
217
+ showToast(`Saved, but git-sync failed: ${result.syncWarning}`, "warning");
218
+ }
219
+ await refresh();
220
+ } catch (err) {
221
+ showToast(err.message || "Failed to save changes", "error");
222
+ } finally {
223
+ setSaving(false);
224
+ }
225
+ }, [saving, primary, configuredModels, profileEdits, orderEdits, refresh]);
226
+
227
+ const refreshCodexStatus = useCallback(async () => {
228
+ try {
229
+ const codex = await fetchCodexStatus();
230
+ setCodexStatus(codex || { connected: false });
231
+ updateCache({ codexStatus: codex || { connected: false } });
232
+ } catch {
233
+ setCodexStatus({ connected: false });
234
+ updateCache({ codexStatus: { connected: false } });
235
+ }
236
+ }, [updateCache]);
237
+
238
+ return {
239
+ catalog,
240
+ primary,
241
+ configuredModels,
242
+ authProfiles,
243
+ authOrder,
244
+ codexStatus,
245
+ loading,
246
+ saving,
247
+ ready,
248
+ error,
249
+ isDirty,
250
+ refresh,
251
+ addModel,
252
+ removeModel,
253
+ setPrimaryModel,
254
+ editProfile,
255
+ editAuthOrder,
256
+ getProfileValue,
257
+ getEffectiveOrder,
258
+ cancelChanges,
259
+ saveAll,
260
+ refreshCodexStatus,
261
+ };
262
+ };
@@ -268,7 +268,7 @@ export const Models = () => {
268
268
  : selectedModelProvider === "openai"
269
269
  ? !!getKeyVal(envVars, "OPENAI_API_KEY")
270
270
  : selectedModelProvider === "openai-codex"
271
- ? !!(codexStatus.connected || getKeyVal(envVars, "OPENAI_API_KEY"))
271
+ ? !!codexStatus.connected
272
272
  : selectedModelProvider === "google"
273
273
  ? !!getKeyVal(envVars, "GEMINI_API_KEY")
274
274
  : false;
@@ -232,7 +232,7 @@ export const Providers = ({ onRestartRequired = () => {} }) => {
232
232
  : selectedModelProvider === "openai"
233
233
  ? !!getKeyVal(envVars, "OPENAI_API_KEY")
234
234
  : selectedModelProvider === "openai-codex"
235
- ? !!(codexStatus.connected || getKeyVal(envVars, "OPENAI_API_KEY"))
235
+ ? !!codexStatus.connected
236
236
  : selectedModelProvider === "google"
237
237
  ? !!getKeyVal(envVars, "GEMINI_API_KEY")
238
238
  : false;
@@ -0,0 +1,35 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { FileViewer } from "../file-viewer/index.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ export const BrowseRoute = ({
8
+ activeBrowsePath = "",
9
+ browseView = "edit",
10
+ lineTarget = 0,
11
+ lineEndTarget = 0,
12
+ selectedBrowsePath = "",
13
+ onNavigateToBrowseFile = () => {},
14
+ onEditSelectedBrowseFile = () => {},
15
+ onClearSelection = () => {},
16
+ }) => html`
17
+ <div class="w-full">
18
+ <${FileViewer}
19
+ filePath=${activeBrowsePath}
20
+ isPreviewOnly=${false}
21
+ browseView=${browseView}
22
+ lineTarget=${lineTarget}
23
+ lineEndTarget=${lineEndTarget}
24
+ onRequestEdit=${(targetPath) => {
25
+ const normalizedTargetPath = String(targetPath || "");
26
+ if (normalizedTargetPath && normalizedTargetPath !== selectedBrowsePath) {
27
+ onNavigateToBrowseFile(normalizedTargetPath, { view: "edit" });
28
+ return;
29
+ }
30
+ onEditSelectedBrowseFile();
31
+ }}
32
+ onRequestClearSelection=${onClearSelection}
33
+ />
34
+ </div>
35
+ `;
@@ -0,0 +1,21 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { DoctorTab } from "../doctor/index.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ export const DoctorRoute = ({ onNavigateToBrowseFile = () => {} }) => html`
8
+ <div class="pt-4">
9
+ <${DoctorTab}
10
+ isActive=${true}
11
+ onOpenFile=${(relativePath, options = {}) => {
12
+ const browsePath = `workspace/${String(relativePath || "").trim().replace(/^workspace\//, "")}`;
13
+ onNavigateToBrowseFile(browsePath, {
14
+ view: "edit",
15
+ ...(options.line ? { line: options.line } : {}),
16
+ ...(options.lineEnd ? { lineEnd: options.lineEnd } : {}),
17
+ });
18
+ }}
19
+ />
20
+ </div>
21
+ `;
@@ -0,0 +1,11 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { Envars } from "../envars.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ export const EnvarsRoute = ({ onRestartRequired = () => {} }) => html`
8
+ <div class="pt-4">
9
+ <${Envars} onRestartRequired=${onRestartRequired} />
10
+ </div>
11
+ `;
@@ -0,0 +1,45 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { GeneralTab } from "../general/index.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ export const GeneralRoute = ({
8
+ statusData = null,
9
+ watchdogData = null,
10
+ doctorStatusData = null,
11
+ doctorWarningDismissedUntilMs = 0,
12
+ onRefreshStatuses = () => {},
13
+ onSetLocation = () => {},
14
+ onNavigate = () => {},
15
+ restartingGateway = false,
16
+ onRestartGateway = () => {},
17
+ restartSignal = 0,
18
+ openclawUpdateInProgress = false,
19
+ onOpenclawVersionActionComplete = () => {},
20
+ onOpenclawUpdate = () => {},
21
+ onRestartRequired = () => {},
22
+ onDismissDoctorWarning = () => {},
23
+ }) => html`
24
+ <div class="pt-4">
25
+ <${GeneralTab}
26
+ statusData=${statusData}
27
+ watchdogData=${watchdogData}
28
+ doctorStatusData=${doctorStatusData}
29
+ doctorWarningDismissedUntilMs=${doctorWarningDismissedUntilMs}
30
+ onRefreshStatuses=${onRefreshStatuses}
31
+ onSwitchTab=${(nextTab) => onSetLocation(`/${nextTab}`)}
32
+ onNavigate=${onNavigate}
33
+ onOpenGmailWebhook=${() => onSetLocation("/webhooks/gmail")}
34
+ isActive=${true}
35
+ restartingGateway=${restartingGateway}
36
+ onRestartGateway=${onRestartGateway}
37
+ restartSignal=${restartSignal}
38
+ openclawUpdateInProgress=${openclawUpdateInProgress}
39
+ onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
40
+ onOpenclawUpdate=${onOpenclawUpdate}
41
+ onRestartRequired=${onRestartRequired}
42
+ onDismissDoctorWarning=${onDismissDoctorWarning}
43
+ />
44
+ </div>
45
+ `;
@@ -0,0 +1,11 @@
1
+ export { BrowseRoute } from "./browse-route.js";
2
+ export { DoctorRoute } from "./doctor-route.js";
3
+ export { EnvarsRoute } from "./envars-route.js";
4
+ export { GeneralRoute } from "./general-route.js";
5
+ export { ModelsRoute } from "./models-route.js";
6
+ export { ProvidersRoute } from "./providers-route.js";
7
+ export { RouteRedirect } from "./route-redirect.js";
8
+ export { TelegramRoute } from "./telegram-route.js";
9
+ export { UsageRoute } from "./usage-route.js";
10
+ export { WatchdogRoute } from "./watchdog-route.js";
11
+ export { WebhooksRoute } from "./webhooks-route.js";
@@ -0,0 +1,11 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { Models } from "../models-tab/index.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ export const ModelsRoute = ({ onRestartRequired = () => {} }) => html`
8
+ <div class="pt-4">
9
+ <${Models} onRestartRequired=${onRestartRequired} />
10
+ </div>
11
+ `;
@@ -0,0 +1,11 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { Providers } from "../providers.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ export const ProvidersRoute = ({ onRestartRequired = () => {} }) => html`
8
+ <div class="pt-4">
9
+ <${Providers} onRestartRequired=${onRestartRequired} />
10
+ </div>
11
+ `;
@@ -0,0 +1,10 @@
1
+ import { useEffect } from "https://esm.sh/preact/hooks";
2
+ import { useLocation } from "https://esm.sh/wouter-preact";
3
+
4
+ export const RouteRedirect = ({ to }) => {
5
+ const [, setLocation] = useLocation();
6
+ useEffect(() => {
7
+ setLocation(to);
8
+ }, [to, setLocation]);
9
+ return null;
10
+ };
@@ -0,0 +1,11 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { TelegramWorkspace } from "../telegram-workspace/index.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ export const TelegramRoute = ({ onBack = () => {} }) => html`
8
+ <div class="pt-4">
9
+ <${TelegramWorkspace} onBack=${onBack} />
10
+ </div>
11
+ `;
@@ -0,0 +1,15 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { UsageTab } from "../usage-tab/index.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ export const UsageRoute = ({ sessionId = "", onSetLocation = () => {} }) => html`
8
+ <div class="pt-4">
9
+ <${UsageTab}
10
+ sessionId=${sessionId}
11
+ onSelectSession=${(id) => onSetLocation(`/usage/${encodeURIComponent(String(id || ""))}`)}
12
+ onBackToSessions=${() => onSetLocation("/usage")}
13
+ />
14
+ </div>
15
+ `;
@@ -0,0 +1,32 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { WatchdogTab } from "../watchdog-tab.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ export const WatchdogRoute = ({
8
+ statusData = null,
9
+ watchdogStatus = null,
10
+ onRefreshStatuses = () => {},
11
+ restartingGateway = false,
12
+ onRestartGateway = () => {},
13
+ restartSignal = 0,
14
+ openclawUpdateInProgress = false,
15
+ onOpenclawVersionActionComplete = () => {},
16
+ onOpenclawUpdate = () => {},
17
+ }) => html`
18
+ <div class="pt-4">
19
+ <${WatchdogTab}
20
+ gatewayStatus=${statusData?.gateway || null}
21
+ openclawVersion=${statusData?.openclawVersion || null}
22
+ watchdogStatus=${watchdogStatus}
23
+ onRefreshStatuses=${onRefreshStatuses}
24
+ restartingGateway=${restartingGateway}
25
+ onRestartGateway=${onRestartGateway}
26
+ restartSignal=${restartSignal}
27
+ openclawUpdateInProgress=${openclawUpdateInProgress}
28
+ onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
29
+ onOpenclawUpdate=${onOpenclawUpdate}
30
+ />
31
+ </div>
32
+ `;
@@ -0,0 +1,43 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { Webhooks } from "../webhooks.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ export const WebhooksRoute = ({
8
+ hookName = "",
9
+ routeHistoryRef = null,
10
+ getCurrentPath = () => "",
11
+ onSetLocation = () => {},
12
+ onRestartRequired = () => {},
13
+ onNavigateToBrowseFile = () => {},
14
+ }) => {
15
+ const handleBackToList = () => {
16
+ const historyStack = routeHistoryRef?.current || [];
17
+ const hasPreviousRoute = historyStack.length > 1;
18
+ if (!hasPreviousRoute) {
19
+ onSetLocation("/webhooks");
20
+ return;
21
+ }
22
+ const currentPath = getCurrentPath();
23
+ window.history.back();
24
+ window.setTimeout(() => {
25
+ if (getCurrentPath() === currentPath) {
26
+ onSetLocation("/webhooks");
27
+ }
28
+ }, 180);
29
+ };
30
+
31
+ return html`
32
+ <div class="pt-4">
33
+ <${Webhooks}
34
+ selectedHookName=${hookName}
35
+ onSelectHook=${(name) => onSetLocation(`/webhooks/${encodeURIComponent(name)}`)}
36
+ onBackToList=${handleBackToList}
37
+ onRestartRequired=${onRestartRequired}
38
+ onOpenFile=${(relativePath) =>
39
+ onNavigateToBrowseFile(String(relativePath || "").trim(), { view: "edit" })}
40
+ />
41
+ </div>
42
+ `;
43
+ };
@@ -47,7 +47,6 @@ export const AppSidebar = ({
47
47
  onPreviewBrowseFile = () => {},
48
48
  acHasUpdate = false,
49
49
  acLatest = "",
50
- acDismissed = false,
51
50
  acUpdating = false,
52
51
  onAcUpdate = () => {},
53
52
  }) => {
@@ -200,7 +199,7 @@ export const AppSidebar = ({
200
199
  `,
201
200
  )}
202
201
  <div class="sidebar-footer">
203
- ${acHasUpdate && acLatest && !acDismissed
202
+ ${acHasUpdate && acLatest
204
203
  ? html`
205
204
  <${UpdateActionButton}
206
205
  onClick=${onAcUpdate}
@@ -251,7 +250,7 @@ export const AppSidebar = ({
251
250
  onSelectFile=${onSelectBrowseFile}
252
251
  isActive=${sidebarTab === "browse"}
253
252
  />
254
- ${acHasUpdate && acLatest && !acDismissed
253
+ ${acHasUpdate && acLatest
255
254
  ? html`
256
255
  <${UpdateActionButton}
257
256
  onClick=${onAcUpdate}
@@ -0,0 +1,106 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useEffect, useRef, useState } from "https://esm.sh/preact/hooks";
3
+ import { createPortal } from "https://esm.sh/preact/compat";
4
+ import htm from "https://esm.sh/htm";
5
+
6
+ const html = htm.bind(h);
7
+
8
+ const kViewportPadding = 8;
9
+ const kTooltipOffset = 8;
10
+
11
+ const getTooltipPosition = (triggerEl, tooltipEl) => {
12
+ if (!triggerEl) return null;
13
+ const triggerRect = triggerEl.getBoundingClientRect();
14
+ const tooltipRect = tooltipEl?.getBoundingClientRect?.() || {
15
+ width: 0,
16
+ height: 0,
17
+ };
18
+ const minLeft = kViewportPadding + tooltipRect.width / 2;
19
+ const maxLeft = window.innerWidth - kViewportPadding - tooltipRect.width / 2;
20
+ const centeredLeft = triggerRect.left + triggerRect.width / 2;
21
+ const left = tooltipRect.width
22
+ ? Math.min(Math.max(centeredLeft, minLeft), maxLeft)
23
+ : centeredLeft;
24
+
25
+ let top = triggerRect.bottom + kTooltipOffset;
26
+ const canRenderAbove =
27
+ triggerRect.top - kTooltipOffset - tooltipRect.height >= kViewportPadding;
28
+ const wouldOverflowBelow =
29
+ top + tooltipRect.height + kViewportPadding > window.innerHeight;
30
+ if (wouldOverflowBelow && canRenderAbove) {
31
+ top = triggerRect.top - kTooltipOffset - tooltipRect.height;
32
+ }
33
+
34
+ return {
35
+ left: `${left}px`,
36
+ top: `${Math.max(kViewportPadding, top)}px`,
37
+ };
38
+ };
39
+
40
+ export const Tooltip = ({
41
+ text = "",
42
+ widthClass = "w-64",
43
+ tooltipClassName = "",
44
+ children = null,
45
+ disabled = false,
46
+ }) => {
47
+ const triggerRef = useRef(null);
48
+ const tooltipRef = useRef(null);
49
+ const [open, setOpen] = useState(false);
50
+ const [positionStyle, setPositionStyle] = useState(null);
51
+
52
+ useEffect(() => {
53
+ if (!open || disabled || !text) return undefined;
54
+
55
+ const updatePosition = () => {
56
+ const nextStyle = getTooltipPosition(triggerRef.current, tooltipRef.current);
57
+ if (nextStyle) setPositionStyle(nextStyle);
58
+ };
59
+
60
+ updatePosition();
61
+ window.addEventListener("resize", updatePosition);
62
+ window.addEventListener("scroll", updatePosition, true);
63
+ return () => {
64
+ window.removeEventListener("resize", updatePosition);
65
+ window.removeEventListener("scroll", updatePosition, true);
66
+ };
67
+ }, [open, disabled, text]);
68
+
69
+ const handleOpen = () => {
70
+ if (disabled || !text) return;
71
+ setOpen(true);
72
+ };
73
+
74
+ const handleClose = () => setOpen(false);
75
+
76
+ return html`
77
+ <span
78
+ ref=${triggerRef}
79
+ class="inline-flex"
80
+ onMouseEnter=${handleOpen}
81
+ onMouseLeave=${handleClose}
82
+ onFocusIn=${handleOpen}
83
+ onFocusOut=${(event) => {
84
+ if (event.currentTarget.contains(event.relatedTarget)) return;
85
+ handleClose();
86
+ }}
87
+ >
88
+ ${children}
89
+ ${open && !disabled && text && typeof document !== "undefined"
90
+ ? createPortal(
91
+ html`
92
+ <span
93
+ ref=${tooltipRef}
94
+ role="tooltip"
95
+ class=${`pointer-events-none fixed left-0 top-0 z-[80] -translate-x-1/2 rounded-md border border-border bg-modal px-2 py-1 text-[11px] text-gray-300 shadow-lg ${widthClass} ${tooltipClassName}`.trim()}
96
+ style=${positionStyle || { visibility: "hidden" }}
97
+ >
98
+ ${text}
99
+ </span>
100
+ `,
101
+ document.body,
102
+ )
103
+ : null}
104
+ </span>
105
+ `;
106
+ };