@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
@@ -1,681 +1,71 @@
1
1
  import { h, render } from "https://esm.sh/preact";
2
- import { useState, useEffect, useRef, useCallback } from "https://esm.sh/preact/hooks";
2
+ import { useState, useEffect } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
4
  import { Router, Route, Switch, useLocation } from "https://esm.sh/wouter-preact";
5
- import {
6
- fetchStatus,
7
- fetchPairings,
8
- approvePairing,
9
- rejectPairing,
10
- fetchDevicePairings,
11
- approveDevice,
12
- rejectDevice,
13
- fetchOnboardStatus,
14
- fetchAuthStatus,
15
- logout,
16
- fetchDashboardUrl,
17
- updateSyncCron,
18
- fetchAlphaclawVersion,
19
- updateAlphaclaw,
20
- fetchRestartStatus,
21
- restartGateway,
22
- fetchWatchdogStatus,
23
- fetchDoctorStatus,
24
- triggerWatchdogRepair,
25
- updateOpenclaw,
26
- } from "./lib/api.js";
27
- import { usePolling } from "./hooks/usePolling.js";
28
- import { Gateway } from "./components/gateway.js";
29
- import { Channels, ALL_CHANNELS } from "./components/channels.js";
30
- import { Pairings } from "./components/pairings.js";
31
- import { DevicePairings } from "./components/device-pairings.js";
32
- import { Google } from "./components/google/index.js";
33
- import { Features } from "./components/features.js";
34
- import { Providers } from "./components/providers.js";
5
+ import { logout } from "./lib/api.js";
35
6
  import { Welcome } from "./components/welcome.js";
36
- import { Envars } from "./components/envars.js";
37
- import { Webhooks } from "./components/webhooks.js";
38
- import { ToastContainer, showToast } from "./components/toast.js";
39
- import { TelegramWorkspace } from "./components/telegram-workspace/index.js";
40
- import { ChevronDownIcon } from "./components/icons.js";
41
- import { UpdateActionButton } from "./components/update-action-button.js";
7
+ import { ToastContainer } from "./components/toast.js";
42
8
  import { GlobalRestartBanner } from "./components/global-restart-banner.js";
43
9
  import { LoadingSpinner } from "./components/loading-spinner.js";
44
- import { WatchdogTab } from "./components/watchdog-tab.js";
45
- import { FileViewer } from "./components/file-viewer/index.js";
46
10
  import { AppSidebar } from "./components/sidebar.js";
47
- import { UsageTab } from "./components/usage-tab/index.js";
48
- import { DoctorTab } from "./components/doctor/index.js";
49
- import { GeneralDoctorWarning } from "./components/doctor/general-warning.js";
11
+ import {
12
+ BrowseRoute,
13
+ DoctorRoute,
14
+ EnvarsRoute,
15
+ GeneralRoute,
16
+ ModelsRoute,
17
+ ProvidersRoute,
18
+ RouteRedirect,
19
+ TelegramRoute,
20
+ UsageRoute,
21
+ WatchdogRoute,
22
+ WebhooksRoute,
23
+ } from "./components/routes/index.js";
24
+ import { useAppShellController } from "./hooks/use-app-shell-controller.js";
25
+ import { useAppShellUi } from "./hooks/use-app-shell-ui.js";
26
+ import { useBrowseNavigation } from "./hooks/use-browse-navigation.js";
27
+ import { getHashRouterPath, useHashLocation } from "./hooks/use-hash-location.js";
50
28
  import { readUiSettings, writeUiSettings } from "./lib/ui-settings.js";
29
+
51
30
  const html = htm.bind(h);
52
- const kDefaultUiTab = "general";
53
- const kDefaultSidebarWidthPx = 220;
54
- const kSidebarMinWidthPx = 180;
55
- const kSidebarMaxWidthPx = 460;
56
- const kBrowseLastPathUiSettingKey = "browseLastPath";
57
- const kLastMenuRouteUiSettingKey = "lastMenuRoute";
58
31
  const kDoctorWarningDismissedUntilUiSettingKey = "doctorWarningDismissedUntilMs";
59
32
  const kOneWeekMs = 7 * 24 * 60 * 60 * 1000;
60
- const kBrowseRestartRequiredRules = [
61
- { type: "file", path: "openclaw.json" },
62
- { type: "directory", path: "hooks/transforms" },
63
- ];
64
- const normalizeBrowsePath = (value) => String(value || "").replace(/^\/+|\/+$/g, "");
65
- const normalizeRestartRulePath = (value) =>
66
- String(value || "")
67
- .trim()
68
- .replace(/^\/+|\/+$/g, "");
69
- const matchesBrowseRestartRequiredRule = (path, rule) => {
70
- const normalizedPath = normalizeRestartRulePath(path);
71
- if (!normalizedPath) return false;
72
- if (!rule || typeof rule !== "object") return false;
73
- const type = String(rule.type || "").toLowerCase();
74
- const targetPath = normalizeRestartRulePath(rule.path);
75
- if (!targetPath) return false;
76
- if (type === "directory") {
77
- return normalizedPath === targetPath || normalizedPath.startsWith(`${targetPath}/`);
78
- }
79
- if (type === "file") {
80
- return normalizedPath === targetPath;
81
- }
82
- return false;
83
- };
84
- const shouldRequireRestartForBrowsePath = (path) =>
85
- kBrowseRestartRequiredRules.some((rule) => matchesBrowseRestartRequiredRule(path, rule));
86
-
87
- const clampSidebarWidth = (value) =>
88
- Math.max(kSidebarMinWidthPx, Math.min(kSidebarMaxWidthPx, value));
89
-
90
- const getHashPath = () => {
91
- const hash = window.location.hash.replace(/^#/, "");
92
- if (!hash) return `/${kDefaultUiTab}`;
93
- return hash.startsWith("/") ? hash : `/${hash}`;
94
- };
95
-
96
- const useHashLocation = () => {
97
- const [location, setLocationState] = useState(getHashPath);
98
-
99
- useEffect(() => {
100
- const onHashChange = () => setLocationState(getHashPath());
101
- window.addEventListener("hashchange", onHashChange);
102
- return () => window.removeEventListener("hashchange", onHashChange);
103
- }, []);
104
-
105
- const setLocation = useCallback((to) => {
106
- const normalized = to.startsWith("/") ? to : `/${to}`;
107
- const nextHash = `#${normalized}`;
108
- if (window.location.hash !== nextHash) {
109
- window.location.hash = normalized;
110
- return;
111
- }
112
- setLocationState(normalized);
113
- }, []);
114
-
115
- return [location, setLocation];
116
- };
117
-
118
- const RouteRedirect = ({ to }) => {
119
- const [, setLocation] = useLocation();
120
- useEffect(() => {
121
- setLocation(to);
122
- }, [to, setLocation]);
123
- return null;
124
- };
125
-
126
- const GeneralTab = ({
127
- statusData = null,
128
- watchdogData = null,
129
- doctorStatusData = null,
130
- doctorWarningDismissedUntilMs = 0,
131
- onRefreshStatuses = () => {},
132
- onSwitchTab,
133
- onNavigate,
134
- onOpenGmailWebhook = () => {},
135
- isActive,
136
- restartingGateway,
137
- onRestartGateway,
138
- restartSignal = 0,
139
- openclawUpdateInProgress = false,
140
- onOpenclawVersionActionComplete = () => {},
141
- onOpenclawUpdate,
142
- onRestartRequired = () => {},
143
- onDismissDoctorWarning = () => {},
144
- }) => {
145
- const [dashboardLoading, setDashboardLoading] = useState(false);
146
- const [repairingWatchdog, setRepairingWatchdog] = useState(false);
147
- const status = statusData;
148
- const watchdogStatus = watchdogData;
149
- const doctorStatus = doctorStatusData;
150
- const gatewayStatus = status?.gateway ?? null;
151
- const channels = status?.channels ?? null;
152
- const repo = status?.repo || null;
153
- const syncCron = status?.syncCron || null;
154
- const openclawVersion = status?.openclawVersion || null;
155
- const [syncCronEnabled, setSyncCronEnabled] = useState(true);
156
- const [syncCronSchedule, setSyncCronSchedule] = useState("0 * * * *");
157
- const [savingSyncCron, setSavingSyncCron] = useState(false);
158
- const [syncCronChoice, setSyncCronChoice] = useState("0 * * * *");
159
-
160
- const hasUnpaired = ALL_CHANNELS.some((ch) => {
161
- const info = channels?.[ch];
162
- return info && info.status !== "paired";
163
- });
164
-
165
- const pairingsPoll = usePolling(
166
- async () => {
167
- const d = await fetchPairings();
168
- return d.pending || [];
169
- },
170
- 1000,
171
- { enabled: hasUnpaired && gatewayStatus === "running" },
172
- );
173
- const pending = pairingsPoll.data || [];
174
-
175
- const refreshAfterAction = () => {
176
- setTimeout(pairingsPoll.refresh, 500);
177
- setTimeout(pairingsPoll.refresh, 2000);
178
- setTimeout(onRefreshStatuses, 3000);
179
- };
180
-
181
- const handleApprove = async (id, channel) => {
182
- await approvePairing(id, channel);
183
- refreshAfterAction();
184
- };
185
-
186
- const handleReject = async (id, channel) => {
187
- await rejectPairing(id, channel);
188
- refreshAfterAction();
189
- };
190
-
191
- const devicePoll = usePolling(
192
- async () => {
193
- const d = await fetchDevicePairings();
194
- return d.pending || [];
195
- },
196
- 2000,
197
- { enabled: gatewayStatus === "running" },
198
- );
199
- const devicePending = devicePoll.data || [];
200
-
201
- const handleDeviceApprove = async (id) => {
202
- await approveDevice(id);
203
- setTimeout(devicePoll.refresh, 500);
204
- setTimeout(devicePoll.refresh, 2000);
205
- };
206
-
207
- const handleDeviceReject = async (id) => {
208
- await rejectDevice(id);
209
- setTimeout(devicePoll.refresh, 500);
210
- setTimeout(devicePoll.refresh, 2000);
211
- };
212
-
213
- useEffect(() => {
214
- if (!isActive) return;
215
- onRefreshStatuses();
216
- pairingsPoll.refresh();
217
- devicePoll.refresh();
218
- }, [isActive]);
219
-
220
- useEffect(() => {
221
- if (!restartSignal || !isActive) return;
222
- onRefreshStatuses();
223
- pairingsPoll.refresh();
224
- devicePoll.refresh();
225
- const t1 = setTimeout(() => {
226
- onRefreshStatuses();
227
- pairingsPoll.refresh();
228
- devicePoll.refresh();
229
- }, 1200);
230
- const t2 = setTimeout(() => {
231
- onRefreshStatuses();
232
- pairingsPoll.refresh();
233
- devicePoll.refresh();
234
- }, 3500);
235
- return () => {
236
- clearTimeout(t1);
237
- clearTimeout(t2);
238
- };
239
- }, [
240
- restartSignal,
241
- isActive,
242
- onRefreshStatuses,
243
- pairingsPoll.refresh,
244
- devicePoll.refresh,
245
- ]);
246
-
247
- useEffect(() => {
248
- if (!syncCron) return;
249
- setSyncCronEnabled(syncCron.enabled !== false);
250
- setSyncCronSchedule(syncCron.schedule || "0 * * * *");
251
- setSyncCronChoice(
252
- syncCron.enabled === false
253
- ? "disabled"
254
- : syncCron.schedule || "0 * * * *",
255
- );
256
- }, [syncCron?.enabled, syncCron?.schedule]);
257
-
258
- const saveSyncCronSettings = async ({
259
- enabled = syncCronEnabled,
260
- schedule = syncCronSchedule,
261
- }) => {
262
- if (savingSyncCron) return;
263
- setSavingSyncCron(true);
264
- try {
265
- const data = await updateSyncCron({ enabled, schedule });
266
- if (!data.ok)
267
- throw new Error(data.error || "Could not save sync settings");
268
- showToast("Sync schedule updated", "success");
269
- onRefreshStatuses();
270
- } catch (err) {
271
- showToast(err.message || "Could not save sync settings", "error");
272
- }
273
- setSavingSyncCron(false);
274
- };
275
-
276
- const syncCronStatusText = syncCronEnabled ? "Enabled" : "Disabled";
277
- const handleWatchdogRepair = async () => {
278
- if (repairingWatchdog) return;
279
- setRepairingWatchdog(true);
280
- try {
281
- const data = await triggerWatchdogRepair();
282
- if (!data.ok) throw new Error(data.error || "Repair failed");
283
- showToast("Repair triggered", "success");
284
- setTimeout(() => {
285
- onRefreshStatuses();
286
- }, 800);
287
- } catch (err) {
288
- showToast(err.message || "Could not run repair", "error");
289
- } finally {
290
- setRepairingWatchdog(false);
291
- }
292
- };
293
-
294
- return html`
295
- <div class="space-y-4">
296
- <${Gateway}
297
- status=${gatewayStatus}
298
- openclawVersion=${openclawVersion}
299
- restarting=${restartingGateway}
300
- onRestart=${onRestartGateway}
301
- watchdogStatus=${watchdogStatus}
302
- onOpenWatchdog=${() => onSwitchTab("watchdog")}
303
- onRepair=${handleWatchdogRepair}
304
- repairing=${repairingWatchdog}
305
- openclawUpdateInProgress=${openclawUpdateInProgress}
306
- onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
307
- onOpenclawUpdate=${onOpenclawUpdate}
308
- />
309
- <${GeneralDoctorWarning}
310
- doctorStatus=${doctorStatus}
311
- dismissedUntilMs=${doctorWarningDismissedUntilMs}
312
- onOpenDoctor=${() => onSwitchTab("doctor")}
313
- onDismiss=${onDismissDoctorWarning}
314
- />
315
- <${Channels} channels=${channels} onSwitchTab=${onSwitchTab} onNavigate=${onNavigate} />
316
- <${Pairings}
317
- pending=${pending}
318
- channels=${channels}
319
- visible=${hasUnpaired}
320
- onApprove=${handleApprove}
321
- onReject=${handleReject}
322
- />
323
- <${Features} onSwitchTab=${onSwitchTab} />
324
- <${Google}
325
- gatewayStatus=${gatewayStatus}
326
- onRestartRequired=${onRestartRequired}
327
- onOpenGmailWebhook=${onOpenGmailWebhook}
328
- />
329
-
330
- ${repo &&
331
- html`
332
- <div class="bg-surface border border-border rounded-xl p-4">
333
- <div class="flex items-center justify-between gap-3">
334
- <div class="flex items-center gap-2 min-w-0">
335
- <svg
336
- class="w-4 h-4 text-gray-400"
337
- viewBox="0 0 16 16"
338
- fill="currentColor"
339
- >
340
- <path
341
- d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
342
- />
343
- </svg>
344
- <a
345
- href="https://github.com/${repo}"
346
- target="_blank"
347
- class="text-sm text-gray-400 hover:text-gray-200 transition-colors truncate"
348
- >${repo}</a
349
- >
350
- </div>
351
- <div class="flex items-center gap-2 shrink-0">
352
- <span class="text-xs text-gray-400">Auto-sync</span>
353
- <div class="relative">
354
- <select
355
- value=${syncCronChoice}
356
- onchange=${(e) => {
357
- const nextChoice = e.target.value;
358
- setSyncCronChoice(nextChoice);
359
- const nextEnabled = nextChoice !== "disabled";
360
- const nextSchedule = nextEnabled
361
- ? nextChoice
362
- : syncCronSchedule;
363
- setSyncCronEnabled(nextEnabled);
364
- setSyncCronSchedule(nextSchedule);
365
- saveSyncCronSettings({
366
- enabled: nextEnabled,
367
- schedule: nextSchedule,
368
- });
369
- }}
370
- disabled=${savingSyncCron}
371
- class="appearance-none bg-black/30 border border-border rounded-lg pl-2.5 pr-9 py-1.5 text-xs text-gray-300 ${savingSyncCron
372
- ? "opacity-50 cursor-not-allowed"
373
- : ""}"
374
- title=${syncCron?.installed === false
375
- ? "Not Installed Yet"
376
- : syncCronStatusText}
377
- >
378
- <option value="disabled">Disabled</option>
379
- <option value="*/30 * * * *">Every 30 min</option>
380
- <option value="0 * * * *">Hourly</option>
381
- <option value="0 0 * * *">Daily</option>
382
- </select>
383
- <${ChevronDownIcon}
384
- className="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-500"
385
- />
386
- </div>
387
- </div>
388
- </div>
389
- </div>
390
- `}
391
-
392
- <div class="bg-surface border border-border rounded-xl p-4">
393
- <div class="flex items-center justify-between">
394
- <div>
395
- <h2 class="font-semibold text-sm">OpenClaw Gateway Dashboard</h2>
396
- </div>
397
- <${UpdateActionButton}
398
- onClick=${async () => {
399
- if (dashboardLoading) return;
400
- setDashboardLoading(true);
401
- try {
402
- const data = await fetchDashboardUrl();
403
- console.log("[dashboard] response:", JSON.stringify(data));
404
- window.open(data.url || "/openclaw", "_blank");
405
- } catch (err) {
406
- console.error("[dashboard] error:", err);
407
- window.open("/openclaw", "_blank");
408
- }
409
- setDashboardLoading(false);
410
- }}
411
- loading=${dashboardLoading}
412
- warning=${false}
413
- idleLabel="Open"
414
- loadingLabel="Opening..."
415
- />
416
- </div>
417
- <${DevicePairings}
418
- pending=${devicePending}
419
- onApprove=${handleDeviceApprove}
420
- onReject=${handleDeviceReject}
421
- />
422
- </div>
423
- </div>
424
- `;
425
- };
426
33
 
427
34
  const App = () => {
428
- const appShellRef = useRef(null);
429
- const [onboarded, setOnboarded] = useState(null);
430
35
  const [location, setLocation] = useLocation();
431
- const [acVersion, setAcVersion] = useState(null);
432
- const [acLatest, setAcLatest] = useState(null);
433
- const [acHasUpdate, setAcHasUpdate] = useState(false);
434
- const [acUpdating, setAcUpdating] = useState(false);
435
- const [acDismissed, setAcDismissed] = useState(false);
436
- const [authEnabled, setAuthEnabled] = useState(false);
437
- const [menuOpen, setMenuOpen] = useState(false);
438
- const [sidebarTab, setSidebarTab] = useState(() =>
439
- location.startsWith("/browse") ? "browse" : "menu",
440
- );
441
- const [sidebarWidthPx, setSidebarWidthPx] = useState(() => {
442
- const settings = readUiSettings();
443
- if (!Number.isFinite(settings.sidebarWidthPx)) return kDefaultSidebarWidthPx;
444
- return clampSidebarWidth(settings.sidebarWidthPx);
445
- });
446
- const [lastBrowsePath, setLastBrowsePath] = useState(() => {
447
- const settings = readUiSettings();
448
- return typeof settings[kBrowseLastPathUiSettingKey] === "string"
449
- ? settings[kBrowseLastPathUiSettingKey]
450
- : "";
451
- });
452
- const [lastMenuRoute, setLastMenuRoute] = useState(() => {
453
- const settings = readUiSettings();
454
- const storedRoute = settings[kLastMenuRouteUiSettingKey];
455
- if (
456
- typeof storedRoute === "string" &&
457
- storedRoute.startsWith("/") &&
458
- !storedRoute.startsWith("/browse")
459
- ) {
460
- return storedRoute;
461
- }
462
- return `/${kDefaultUiTab}`;
463
- });
464
36
  const [doctorWarningDismissedUntilMs, setDoctorWarningDismissedUntilMs] = useState(() => {
465
37
  const settings = readUiSettings();
466
38
  return Number(settings[kDoctorWarningDismissedUntilUiSettingKey] || 0);
467
39
  });
468
- const [isResizingSidebar, setIsResizingSidebar] = useState(false);
469
- const [browsePreviewPath, setBrowsePreviewPath] = useState("");
470
- const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
471
- const [mobileTopbarScrolled, setMobileTopbarScrolled] = useState(false);
472
- const [restartRequired, setRestartRequired] = useState(false);
473
- const [browseRestartRequired, setBrowseRestartRequired] = useState(false);
474
- const [restartingGateway, setRestartingGateway] = useState(false);
475
- const [gatewayRestartSignal, setGatewayRestartSignal] = useState(0);
476
- const [statusPollCadenceMs, setStatusPollCadenceMs] = useState(15000);
477
- const [openclawUpdateInProgress, setOpenclawUpdateInProgress] = useState(false);
478
- const menuRef = useRef(null);
479
- const routeHistoryRef = useRef([]);
480
- const menuPaneRef = useRef(null);
481
- const sharedStatusPoll = usePolling(fetchStatus, statusPollCadenceMs, {
482
- enabled: onboarded === true,
483
- });
484
- const sharedWatchdogPoll = usePolling(fetchWatchdogStatus, statusPollCadenceMs, {
485
- enabled: onboarded === true,
486
- });
487
- const sharedDoctorPoll = usePolling(fetchDoctorStatus, statusPollCadenceMs, {
488
- enabled: onboarded === true,
489
- });
490
- const sharedStatus = sharedStatusPoll.data || null;
491
- const sharedWatchdogStatus = sharedWatchdogPoll.data?.status || null;
492
- const sharedDoctorStatus = sharedDoctorPoll.data?.status || null;
493
- const isAnyRestartRequired = restartRequired || browseRestartRequired;
494
- const refreshSharedStatuses = useCallback(() => {
495
- sharedStatusPoll.refresh();
496
- sharedWatchdogPoll.refresh();
497
- sharedDoctorPoll.refresh();
498
- }, [sharedStatusPoll.refresh, sharedWatchdogPoll.refresh, sharedDoctorPoll.refresh]);
499
-
500
- const closeMenu = useCallback((e) => {
501
- if (menuRef.current && !menuRef.current.contains(e.target)) {
502
- setMenuOpen(false);
503
- }
504
- }, []);
505
-
506
- useEffect(() => {
507
- if (menuOpen) {
508
- document.addEventListener("click", closeMenu, true);
509
- return () => document.removeEventListener("click", closeMenu, true);
510
- }
511
- }, [menuOpen, closeMenu]);
512
-
513
- useEffect(() => {
514
- fetchOnboardStatus()
515
- .then((data) => setOnboarded(data.onboarded))
516
- .catch(() => setOnboarded(false));
517
- fetchAuthStatus()
518
- .then((data) => setAuthEnabled(!!data.authEnabled))
519
- .catch(() => {});
520
- }, []);
521
-
522
- useEffect(() => {
523
- if (!mobileSidebarOpen) return;
524
- const previousOverflow = document.body.style.overflow;
525
- document.body.style.overflow = "hidden";
526
- return () => {
527
- document.body.style.overflow = previousOverflow;
528
- };
529
- }, [mobileSidebarOpen]);
530
-
531
- useEffect(() => {
532
- if (!onboarded) return;
533
- let active = true;
534
- const check = async (refresh = false) => {
535
- try {
536
- const data = await fetchAlphaclawVersion(refresh);
537
- if (!active) return;
538
- setAcVersion(data.currentVersion || null);
539
- setAcLatest(data.latestVersion || null);
540
- setAcHasUpdate(!!data.hasUpdate);
541
- } catch {}
542
- };
543
- check(true);
544
- const id = setInterval(() => check(false), 5 * 60 * 1000);
545
- return () => {
546
- active = false;
547
- clearInterval(id);
548
- };
549
- }, [onboarded]);
550
-
551
- const refreshRestartStatus = useCallback(async () => {
552
- if (!onboarded) return;
553
- try {
554
- const data = await fetchRestartStatus();
555
- setRestartRequired(!!data.restartRequired);
556
- setRestartingGateway(!!data.restartInProgress);
557
- } catch {}
558
- }, [onboarded]);
559
-
560
- useEffect(() => {
561
- if (!onboarded) return;
562
- refreshRestartStatus();
563
- }, [onboarded, refreshRestartStatus]);
564
40
 
565
- useEffect(() => {
566
- if (onboarded !== true) return;
567
- const inStatusView =
568
- location.startsWith("/general") || location.startsWith("/watchdog");
569
- const gatewayStatus = sharedStatus?.gateway ?? null;
570
- const watchdogHealth = String(sharedWatchdogStatus?.health || "").toLowerCase();
571
- const watchdogLifecycle = String(sharedWatchdogStatus?.lifecycle || "").toLowerCase();
572
- const shouldFastPollWatchdog =
573
- watchdogHealth === "unknown" ||
574
- watchdogLifecycle === "restarting" ||
575
- watchdogLifecycle === "stopped" ||
576
- !!sharedWatchdogStatus?.operationInProgress;
577
- const shouldFastPollGateway = !gatewayStatus || gatewayStatus !== "running";
578
- const nextCadenceMs =
579
- inStatusView && (shouldFastPollWatchdog || shouldFastPollGateway) ? 2000 : 15000;
580
- setStatusPollCadenceMs((currentCadenceMs) =>
581
- currentCadenceMs === nextCadenceMs ? currentCadenceMs : nextCadenceMs,
582
- );
583
- }, [
584
- onboarded,
41
+ const { state: controllerState, actions: controllerActions } = useAppShellController({
585
42
  location,
586
- sharedStatus?.gateway,
587
- sharedWatchdogStatus?.health,
588
- sharedWatchdogStatus?.lifecycle,
589
- sharedWatchdogStatus?.operationInProgress,
590
- ]);
591
-
592
- useEffect(() => {
593
- if (!onboarded || (!restartRequired && !restartingGateway)) return;
594
- const id = setInterval(refreshRestartStatus, 2000);
595
- return () => clearInterval(id);
596
- }, [onboarded, restartRequired, restartingGateway, refreshRestartStatus]);
43
+ });
44
+ const { refs: shellRefs, state: shellState, actions: shellActions } = useAppShellUi();
45
+ const { state: browseState, actions: browseActions, constants: browseConstants } =
46
+ useBrowseNavigation({
47
+ location,
48
+ setLocation,
49
+ onCloseMobileSidebar: shellActions.closeMobileSidebar,
50
+ });
597
51
 
598
52
  useEffect(() => {
599
- const handleBrowseFileSaved = (event) => {
600
- const savedPath = String(event?.detail?.path || "");
601
- if (!shouldRequireRestartForBrowsePath(savedPath)) return;
602
- setBrowseRestartRequired(true);
603
- };
604
- window.addEventListener("alphaclaw:browse-file-saved", handleBrowseFileSaved);
605
- return () => {
606
- window.removeEventListener("alphaclaw:browse-file-saved", handleBrowseFileSaved);
607
- };
608
- }, []);
609
-
610
- const handleGatewayRestart = useCallback(async () => {
611
- if (restartingGateway) return;
612
- setRestartingGateway(true);
613
- try {
614
- const data = await restartGateway();
615
- if (!data?.ok) throw new Error(data?.error || "Gateway restart failed");
616
- setRestartRequired(!!data.restartRequired);
617
- setBrowseRestartRequired(false);
618
- setGatewayRestartSignal(Date.now());
619
- refreshSharedStatuses();
620
- showToast("Gateway restarted", "success");
621
- setTimeout(refreshRestartStatus, 800);
622
- } catch (err) {
623
- showToast(err.message || "Restart failed", "error");
624
- setTimeout(refreshRestartStatus, 800);
625
- } finally {
626
- setRestartingGateway(false);
627
- }
628
- }, [restartingGateway, refreshRestartStatus, refreshSharedStatuses]);
629
-
630
- const handleOpenclawUpdate = useCallback(async () => {
631
- if (openclawUpdateInProgress) {
632
- return { ok: false, error: "OpenClaw update already in progress" };
633
- }
634
- setOpenclawUpdateInProgress(true);
635
- try {
636
- const data = await updateOpenclaw();
637
- return data;
638
- } finally {
639
- setOpenclawUpdateInProgress(false);
640
- refreshSharedStatuses();
641
- setTimeout(refreshSharedStatuses, 1200);
642
- setTimeout(refreshSharedStatuses, 3500);
643
- setTimeout(refreshRestartStatus, 1200);
644
- }
645
- }, [
646
- openclawUpdateInProgress,
647
- refreshRestartStatus,
648
- refreshSharedStatuses,
649
- ]);
650
-
651
- const handleOpenclawVersionActionComplete = useCallback(
652
- ({ type }) => {
653
- if (type !== "update") return;
654
- refreshSharedStatuses();
655
- setTimeout(refreshSharedStatuses, 1200);
656
- },
657
- [refreshSharedStatuses],
658
- );
53
+ const settings = readUiSettings();
54
+ settings[kDoctorWarningDismissedUntilUiSettingKey] = doctorWarningDismissedUntilMs;
55
+ writeUiSettings(settings);
56
+ }, [doctorWarningDismissedUntilMs]);
659
57
 
660
- const handleAcUpdate = async () => {
661
- if (acUpdating) return;
662
- setAcUpdating(true);
58
+ const handleSidebarLogout = async () => {
59
+ shellActions.setMenuOpen(false);
60
+ await logout();
663
61
  try {
664
- const data = await updateAlphaclaw();
665
- if (data.ok) {
666
- showToast("AlphaClaw updated — restarting...", "success");
667
- setTimeout(() => window.location.reload(), 5000);
668
- } else {
669
- showToast(data.error || "AlphaClaw update failed", "error");
670
- setAcUpdating(false);
671
- }
672
- } catch (err) {
673
- showToast(err.message || "Could not update AlphaClaw", "error");
674
- setAcUpdating(false);
675
- }
62
+ window.localStorage.clear();
63
+ window.sessionStorage.clear();
64
+ } catch {}
65
+ window.location.href = "/login.html";
676
66
  };
677
- // Still loading onboard status
678
- if (onboarded === null) {
67
+
68
+ if (controllerState.onboarded === null) {
679
69
  return html`
680
70
  <div
681
71
  class="min-h-screen flex items-center justify-center"
@@ -690,382 +80,95 @@ const App = () => {
690
80
  `;
691
81
  }
692
82
 
693
- if (!onboarded) {
83
+ if (!controllerState.onboarded) {
694
84
  return html`
695
85
  <div
696
86
  class="min-h-screen flex justify-center pt-12 pb-8 px-4"
697
87
  style="position: relative; z-index: 1"
698
88
  >
699
- <${Welcome} onComplete=${() => setOnboarded(true)} />
89
+ <${Welcome} onComplete=${controllerActions.handleOnboardingComplete} />
700
90
  </div>
701
91
  <${ToastContainer} />
702
92
  `;
703
93
  }
704
94
 
705
- const buildBrowseRoute = (relativePath, options = {}) => {
706
- const view = String(options?.view || "edit");
707
- const encodedPath = String(relativePath || "")
708
- .split("/")
709
- .filter(Boolean)
710
- .map((segment) => encodeURIComponent(segment))
711
- .join("/");
712
- const baseRoute = encodedPath ? `/browse/${encodedPath}` : "/browse";
713
- const params = new URLSearchParams();
714
- if (view === "diff" && encodedPath) params.set("view", "diff");
715
- if (options.line) params.set("line", String(options.line));
716
- if (options.lineEnd) params.set("lineEnd", String(options.lineEnd));
717
- const query = params.toString();
718
- return query ? `${baseRoute}?${query}` : baseRoute;
719
- };
720
- const navigateToSubScreen = (screen) => {
721
- setLocation(`/${screen}`);
722
- setMobileSidebarOpen(false);
723
- };
724
- const handleBrowsePreviewFile = useCallback((nextPreviewPath) => {
725
- const normalizedPreviewPath = normalizeBrowsePath(nextPreviewPath);
726
- setBrowsePreviewPath(normalizedPreviewPath);
727
- }, []);
728
- const navigateToBrowseFile = (relativePath, options = {}) => {
729
- const normalizedTargetPath = normalizeBrowsePath(relativePath);
730
- const selectingDirectory =
731
- !!options.directory || String(relativePath || "").trim().endsWith("/");
732
- const shouldPreservePreview = selectingDirectory && !!options.preservePreview;
733
- const activePath = normalizeBrowsePath(
734
- browsePreviewPath || selectedBrowsePath || "",
735
- );
736
- const nextPreviewPath =
737
- shouldPreservePreview && activePath && activePath !== normalizedTargetPath
738
- ? activePath
739
- : "";
740
- setBrowsePreviewPath(nextPreviewPath);
741
- const routeOptions = selectingDirectory
742
- ? { ...options, view: "edit" }
743
- : options;
744
- setLocation(buildBrowseRoute(normalizedTargetPath, routeOptions));
745
- setMobileSidebarOpen(false);
746
- };
747
- const handleSidebarLogout = async () => {
748
- setMenuOpen(false);
749
- await logout();
750
- try {
751
- window.localStorage.clear();
752
- window.sessionStorage.clear();
753
- } catch {}
754
- window.location.href = "/login.html";
755
- };
756
- const handleSelectSidebarTab = (nextTab) => {
757
- setSidebarTab(nextTab);
758
- if (nextTab === "menu" && location.startsWith("/browse")) {
759
- setBrowsePreviewPath("");
760
- setLocation(lastMenuRoute || `/${kDefaultUiTab}`);
761
- return;
762
- }
763
- if (nextTab === "browse" && !location.startsWith("/browse")) {
764
- setLocation(buildBrowseRoute(lastBrowsePath));
765
- }
766
- };
767
- const handleSelectNavItem = (itemId) => {
768
- setLocation(`/${itemId}`);
769
- setMobileSidebarOpen(false);
770
- };
771
- const exitSubScreen = () => {
772
- setLocation(`/${kDefaultUiTab}`);
773
- setMobileSidebarOpen(false);
774
- };
775
- const handlePaneScroll = (e) => {
776
- const nextScrolled = e.currentTarget.scrollTop > 0;
777
- setMobileTopbarScrolled((currentScrolled) =>
778
- currentScrolled === nextScrolled ? currentScrolled : nextScrolled,
779
- );
780
- };
781
-
782
- const kNavSections = [
783
- {
784
- label: "Setup",
785
- items: [
786
- { id: "general", label: "General" },
787
- ],
788
- },
789
- {
790
- label: "Monitoring",
791
- items: [
792
- { id: "watchdog", label: "Watchdog" },
793
- { id: "usage", label: "Usage" },
794
- { id: "doctor", label: "Doctor" },
795
- ],
796
- },
797
- {
798
- label: "Config",
799
- items: [
800
- { id: "providers", label: "Providers" },
801
- { id: "envars", label: "Envars" },
802
- { id: "webhooks", label: "Webhooks" },
803
- ],
804
- },
805
- ];
806
-
807
- const isBrowseRoute = location.startsWith("/browse");
808
- const browseRoutePath = isBrowseRoute ? String(location || "").split("?")[0] : "";
809
- const browseRouteQuery =
810
- isBrowseRoute && String(location || "").includes("?")
811
- ? String(location || "").split("?").slice(1).join("?")
812
- : "";
813
- const selectedBrowsePath = isBrowseRoute
814
- ? browseRoutePath
815
- .replace(/^\/browse\/?/, "")
816
- .split("/")
817
- .filter(Boolean)
818
- .map((segment) => {
819
- try {
820
- return decodeURIComponent(segment);
821
- } catch {
822
- return segment;
823
- }
824
- })
825
- .join("/")
826
- : "";
827
- const activeBrowsePath = browsePreviewPath || selectedBrowsePath;
828
- const browseQueryParams = isBrowseRoute ? new URLSearchParams(browseRouteQuery) : null;
829
- const browseViewerMode =
830
- !browsePreviewPath && browseQueryParams?.get("view") === "diff"
831
- ? "diff"
832
- : "edit";
833
- const browseLineTarget = Number.parseInt(browseQueryParams?.get("line") || "", 10) || 0;
834
- const browseLineEndTarget = Number.parseInt(browseQueryParams?.get("lineEnd") || "", 10) || 0;
835
- const selectedNavId = isBrowseRoute
836
- ? "browse"
837
- : location === "/telegram"
838
- ? ""
839
- : location.startsWith("/providers")
840
- ? "providers"
841
- : location.startsWith("/watchdog")
842
- ? "watchdog"
843
- : location.startsWith("/usage")
844
- ? "usage"
845
- : location.startsWith("/doctor")
846
- ? "doctor"
847
- : location.startsWith("/envars")
848
- ? "envars"
849
- : location.startsWith("/webhooks")
850
- ? "webhooks"
851
- : "general";
852
-
853
- useEffect(() => {
854
- setSidebarTab((currentTab) => {
855
- if (location.startsWith("/browse")) return "browse";
856
- if (currentTab === "browse") return "menu";
857
- return currentTab;
858
- });
859
- }, [location]);
860
-
861
- useEffect(() => {
862
- if (location.startsWith("/browse")) return;
863
- setBrowsePreviewPath("");
864
- }, [location]);
865
-
866
- useEffect(() => {
867
- const historyStack = routeHistoryRef.current;
868
- const lastEntry = historyStack[historyStack.length - 1];
869
- if (lastEntry === location) return;
870
- historyStack.push(location);
871
- if (historyStack.length > 100) {
872
- historyStack.shift();
873
- }
874
- }, [location]);
875
-
876
- useEffect(() => {
877
- if (location.startsWith("/browse")) return;
878
- if (location === "/telegram") return;
879
- setLastMenuRoute((currentRoute) =>
880
- currentRoute === location ? currentRoute : location,
881
- );
882
- }, [location]);
883
-
884
- useEffect(() => {
885
- if (!isBrowseRoute) return;
886
- if (!selectedBrowsePath) return;
887
- setLastBrowsePath((currentPath) =>
888
- currentPath === selectedBrowsePath ? currentPath : selectedBrowsePath,
889
- );
890
- }, [isBrowseRoute, selectedBrowsePath]);
891
-
892
- useEffect(() => {
893
- const handleBrowseGitSynced = () => {
894
- if (!isBrowseRoute || browseViewerMode !== "diff") return;
895
- const activePath = String(selectedBrowsePath || "").trim();
896
- if (!activePath) return;
897
- setLocation(buildBrowseRoute(activePath, { view: "edit" }));
898
- };
899
- window.addEventListener("alphaclaw:browse-git-synced", handleBrowseGitSynced);
900
- return () => {
901
- window.removeEventListener(
902
- "alphaclaw:browse-git-synced",
903
- handleBrowseGitSynced,
904
- );
905
- };
906
- }, [
907
- isBrowseRoute,
908
- browseViewerMode,
909
- selectedBrowsePath,
910
- setLocation,
911
- buildBrowseRoute,
912
- ]);
913
-
914
- useEffect(() => {
915
- const settings = readUiSettings();
916
- settings.sidebarWidthPx = sidebarWidthPx;
917
- settings[kBrowseLastPathUiSettingKey] = lastBrowsePath;
918
- settings[kLastMenuRouteUiSettingKey] = lastMenuRoute;
919
- settings[kDoctorWarningDismissedUntilUiSettingKey] = doctorWarningDismissedUntilMs;
920
- writeUiSettings(settings);
921
- }, [sidebarWidthPx, lastBrowsePath, lastMenuRoute, doctorWarningDismissedUntilMs]);
922
-
923
- const resizeSidebarWithClientX = useCallback((clientX) => {
924
- const shellElement = appShellRef.current;
925
- if (!shellElement) return;
926
- const shellBounds = shellElement.getBoundingClientRect();
927
- const nextWidth = clampSidebarWidth(Math.round(clientX - shellBounds.left));
928
- setSidebarWidthPx(nextWidth);
929
- }, []);
930
-
931
- const onSidebarResizerPointerDown = (event) => {
932
- event.preventDefault();
933
- setIsResizingSidebar(true);
934
- resizeSidebarWithClientX(event.clientX);
935
- };
936
-
937
- useEffect(() => {
938
- if (!isResizingSidebar) return () => {};
939
- const onPointerMove = (event) => resizeSidebarWithClientX(event.clientX);
940
- const onPointerUp = () => setIsResizingSidebar(false);
941
- window.addEventListener("pointermove", onPointerMove);
942
- window.addEventListener("pointerup", onPointerUp);
943
- const previousUserSelect = document.body.style.userSelect;
944
- const previousCursor = document.body.style.cursor;
945
- document.body.style.userSelect = "none";
946
- document.body.style.cursor = "col-resize";
947
- return () => {
948
- window.removeEventListener("pointermove", onPointerMove);
949
- window.removeEventListener("pointerup", onPointerUp);
950
- document.body.style.userSelect = previousUserSelect;
951
- document.body.style.cursor = previousCursor;
952
- };
953
- }, [isResizingSidebar, resizeSidebarWithClientX]);
954
-
955
- const renderWebhooks = (hookName = "") => html`
956
- <div class="pt-4">
957
- <${Webhooks}
958
- selectedHookName=${hookName}
959
- onSelectHook=${(name) => setLocation(`/webhooks/${encodeURIComponent(name)}`)}
960
- onBackToList=${() => {
961
- const historyStack = routeHistoryRef.current;
962
- const hasPreviousRoute = historyStack.length > 1;
963
- if (!hasPreviousRoute) {
964
- setLocation("/webhooks");
965
- return;
966
- }
967
- const currentPath = getHashPath();
968
- window.history.back();
969
- window.setTimeout(() => {
970
- if (getHashPath() === currentPath) {
971
- setLocation("/webhooks");
972
- }
973
- }, 180);
974
- }}
975
- onRestartRequired=${setRestartRequired}
976
- onOpenFile=${(relativePath) =>
977
- navigateToBrowseFile(String(relativePath || "").trim(), { view: "edit" })}
978
- />
979
- </div>
980
- `;
981
-
982
95
  return html`
983
96
  <div
984
97
  class="app-shell"
985
- ref=${appShellRef}
986
- style=${{ "--sidebar-width": `${sidebarWidthPx}px` }}
98
+ ref=${shellRefs.appShellRef}
99
+ style=${{ "--sidebar-width": `${shellState.sidebarWidthPx}px` }}
987
100
  >
988
101
  <${GlobalRestartBanner}
989
- visible=${isAnyRestartRequired}
990
- restarting=${restartingGateway}
991
- onRestart=${handleGatewayRestart}
102
+ visible=${controllerState.isAnyRestartRequired}
103
+ restarting=${controllerState.restartingGateway}
104
+ onRestart=${controllerActions.handleGatewayRestart}
992
105
  />
993
106
  <${AppSidebar}
994
- mobileSidebarOpen=${mobileSidebarOpen}
995
- authEnabled=${authEnabled}
996
- menuRef=${menuRef}
997
- menuOpen=${menuOpen}
998
- onToggleMenu=${() => setMenuOpen((open) => !open)}
107
+ mobileSidebarOpen=${shellState.mobileSidebarOpen}
108
+ authEnabled=${controllerState.authEnabled}
109
+ menuRef=${shellRefs.menuRef}
110
+ menuOpen=${shellState.menuOpen}
111
+ onToggleMenu=${shellActions.onToggleMenu}
999
112
  onLogout=${handleSidebarLogout}
1000
- sidebarTab=${sidebarTab}
1001
- onSelectSidebarTab=${handleSelectSidebarTab}
1002
- navSections=${kNavSections}
1003
- selectedNavId=${selectedNavId}
1004
- onSelectNavItem=${handleSelectNavItem}
1005
- selectedBrowsePath=${selectedBrowsePath}
1006
- onSelectBrowseFile=${navigateToBrowseFile}
1007
- onPreviewBrowseFile=${handleBrowsePreviewFile}
1008
- acHasUpdate=${acHasUpdate}
1009
- acLatest=${acLatest}
1010
- acDismissed=${acDismissed}
1011
- acUpdating=${acUpdating}
1012
- onAcUpdate=${handleAcUpdate}
113
+ sidebarTab=${browseState.sidebarTab}
114
+ onSelectSidebarTab=${browseActions.handleSelectSidebarTab}
115
+ navSections=${browseConstants.kNavSections}
116
+ selectedNavId=${browseState.selectedNavId}
117
+ onSelectNavItem=${browseActions.handleSelectNavItem}
118
+ selectedBrowsePath=${browseState.selectedBrowsePath}
119
+ onSelectBrowseFile=${browseActions.navigateToBrowseFile}
120
+ onPreviewBrowseFile=${browseActions.handleBrowsePreviewFile}
121
+ acHasUpdate=${controllerState.acHasUpdate}
122
+ acLatest=${controllerState.acLatest}
123
+ acUpdating=${controllerState.acUpdating}
124
+ onAcUpdate=${controllerActions.handleAcUpdate}
1013
125
  />
1014
126
  <div
1015
- class=${`sidebar-resizer ${isResizingSidebar ? "is-resizing" : ""}`}
1016
- onpointerdown=${onSidebarResizerPointerDown}
127
+ class=${`sidebar-resizer ${shellState.isResizingSidebar ? "is-resizing" : ""}`}
128
+ onpointerdown=${shellActions.onSidebarResizerPointerDown}
1017
129
  role="separator"
1018
130
  aria-orientation="vertical"
1019
131
  aria-label="Resize sidebar"
1020
132
  ></div>
1021
133
 
1022
134
  <div
1023
- class=${`mobile-sidebar-overlay ${mobileSidebarOpen ? "active" : ""}`}
1024
- onclick=${() => setMobileSidebarOpen(false)}
135
+ class=${`mobile-sidebar-overlay ${shellState.mobileSidebarOpen ? "active" : ""}`}
136
+ onclick=${shellActions.closeMobileSidebar}
1025
137
  />
1026
138
 
1027
139
  <div class="app-content">
1028
140
  <div
1029
141
  class="app-content-pane browse-pane"
1030
- style=${{ display: isBrowseRoute ? "block" : "none" }}
142
+ style=${{ display: browseState.isBrowseRoute ? "block" : "none" }}
1031
143
  >
1032
- <div class="w-full">
1033
- <${FileViewer}
1034
- filePath=${activeBrowsePath}
1035
- isPreviewOnly=${false}
1036
- browseView=${browseViewerMode}
1037
- lineTarget=${browseLineTarget}
1038
- lineEndTarget=${browseLineEndTarget}
1039
- onRequestEdit=${(targetPath) => {
1040
- const normalizedTargetPath = String(targetPath || "");
1041
- if (
1042
- normalizedTargetPath &&
1043
- normalizedTargetPath !== selectedBrowsePath
1044
- ) {
1045
- navigateToBrowseFile(normalizedTargetPath, { view: "edit" });
1046
- return;
1047
- }
1048
- setLocation(buildBrowseRoute(selectedBrowsePath, { view: "edit" }));
1049
- }}
1050
- onRequestClearSelection=${() => {
1051
- setBrowsePreviewPath("");
1052
- setLocation("/browse");
1053
- }}
1054
- />
1055
- </div>
144
+ <${BrowseRoute}
145
+ activeBrowsePath=${browseState.activeBrowsePath}
146
+ browseView=${browseState.browseViewerMode}
147
+ lineTarget=${browseState.browseLineTarget}
148
+ lineEndTarget=${browseState.browseLineEndTarget}
149
+ selectedBrowsePath=${browseState.selectedBrowsePath}
150
+ onNavigateToBrowseFile=${browseActions.navigateToBrowseFile}
151
+ onEditSelectedBrowseFile=${() =>
152
+ setLocation(
153
+ browseActions.buildBrowseRoute(browseState.selectedBrowsePath, { view: "edit" }),
154
+ )}
155
+ onClearSelection=${() => {
156
+ browseActions.clearBrowsePreview();
157
+ setLocation("/browse");
158
+ }}
159
+ />
1056
160
  </div>
1057
161
  <div
1058
162
  class="app-content-pane"
1059
- ref=${menuPaneRef}
1060
- onscroll=${handlePaneScroll}
1061
- style=${{ display: isBrowseRoute ? "none" : "block" }}
163
+ onscroll=${shellActions.handlePaneScroll}
164
+ style=${{ display: browseState.isBrowseRoute ? "none" : "block" }}
1062
165
  >
1063
- <div class=${`mobile-topbar ${mobileTopbarScrolled ? "is-scrolled" : ""}`}>
166
+ <div class=${`mobile-topbar ${shellState.mobileTopbarScrolled ? "is-scrolled" : ""}`}>
1064
167
  <button
1065
168
  class="mobile-topbar-menu"
1066
- onclick=${() => setMobileSidebarOpen((open) => !open)}
169
+ onclick=${() => shellActions.setMobileSidebarOpen((open) => !open)}
1067
170
  aria-label="Open menu"
1068
- aria-expanded=${mobileSidebarOpen ? "true" : "false"}
171
+ aria-expanded=${shellState.mobileSidebarOpen ? "true" : "false"}
1069
172
  >
1070
173
  <svg width="18" height="18" viewBox="0 0 16 16" fill="currentColor">
1071
174
  <path
@@ -1078,106 +181,88 @@ const App = () => {
1078
181
  </span>
1079
182
  </div>
1080
183
  <div class="max-w-2xl w-full mx-auto">
1081
- <div style=${{ display: location === "/general" ? "block" : "none" }}>
1082
- <div class="pt-4">
1083
- <${GeneralTab}
1084
- statusData=${sharedStatus}
1085
- watchdogData=${sharedWatchdogStatus}
1086
- doctorStatusData=${sharedDoctorStatus}
1087
- doctorWarningDismissedUntilMs=${doctorWarningDismissedUntilMs}
1088
- onRefreshStatuses=${refreshSharedStatuses}
1089
- onSwitchTab=${(nextTab) => setLocation(`/${nextTab}`)}
1090
- onNavigate=${navigateToSubScreen}
1091
- onOpenGmailWebhook=${() => setLocation("/webhooks/gmail")}
1092
- isActive=${location === "/general"}
1093
- restartingGateway=${restartingGateway}
1094
- onRestartGateway=${handleGatewayRestart}
1095
- restartSignal=${gatewayRestartSignal}
1096
- openclawUpdateInProgress=${openclawUpdateInProgress}
1097
- onOpenclawVersionActionComplete=${handleOpenclawVersionActionComplete}
1098
- onOpenclawUpdate=${handleOpenclawUpdate}
1099
- onRestartRequired=${setRestartRequired}
1100
- onDismissDoctorWarning=${() =>
1101
- setDoctorWarningDismissedUntilMs(Date.now() + kOneWeekMs)}
1102
- />
1103
- </div>
1104
- </div>
1105
- <div style=${{ display: location === "/doctor" ? "block" : "none" }}>
1106
- <div class="pt-4">
1107
- <${DoctorTab}
1108
- isActive=${location === "/doctor"}
1109
- onOpenFile=${(relativePath, options = {}) => {
1110
- const browsePath = `workspace/${String(relativePath || "").trim().replace(/^workspace\//, "")}`;
1111
- navigateToBrowseFile(browsePath, {
1112
- view: "edit",
1113
- ...(options.line ? { line: options.line } : {}),
1114
- ...(options.lineEnd ? { lineEnd: options.lineEnd } : {}),
1115
- });
1116
- }}
1117
- />
1118
- </div>
1119
- </div>
1120
- ${!isBrowseRoute && location !== "/general" && location !== "/doctor"
184
+ ${!browseState.isBrowseRoute
1121
185
  ? html`
1122
186
  <${Switch}>
187
+ <${Route} path="/general">
188
+ <${GeneralRoute}
189
+ statusData=${controllerState.sharedStatus}
190
+ watchdogData=${controllerState.sharedWatchdogStatus}
191
+ doctorStatusData=${controllerState.sharedDoctorStatus}
192
+ doctorWarningDismissedUntilMs=${doctorWarningDismissedUntilMs}
193
+ onRefreshStatuses=${controllerActions.refreshSharedStatuses}
194
+ onSetLocation=${setLocation}
195
+ onNavigate=${browseActions.navigateToSubScreen}
196
+ restartingGateway=${controllerState.restartingGateway}
197
+ onRestartGateway=${controllerActions.handleGatewayRestart}
198
+ restartSignal=${controllerState.gatewayRestartSignal}
199
+ openclawUpdateInProgress=${controllerState.openclawUpdateInProgress}
200
+ onOpenclawVersionActionComplete=${controllerActions.handleOpenclawVersionActionComplete}
201
+ onOpenclawUpdate=${controllerActions.handleOpenclawUpdate}
202
+ onRestartRequired=${controllerActions.setRestartRequired}
203
+ onDismissDoctorWarning=${() =>
204
+ setDoctorWarningDismissedUntilMs(Date.now() + kOneWeekMs)}
205
+ />
206
+ </${Route}>
207
+ <${Route} path="/doctor">
208
+ <${DoctorRoute} onNavigateToBrowseFile=${browseActions.navigateToBrowseFile} />
209
+ </${Route}>
1123
210
  <${Route} path="/telegram">
1124
- <div class="pt-4">
1125
- <${TelegramWorkspace} onBack=${exitSubScreen} />
1126
- </div>
211
+ <${TelegramRoute} onBack=${browseActions.exitSubScreen} />
212
+ </${Route}>
213
+ <${Route} path="/models">
214
+ <${ModelsRoute} onRestartRequired=${controllerActions.setRestartRequired} />
1127
215
  </${Route}>
1128
216
  <${Route} path="/providers">
1129
- <div class="pt-4">
1130
- <${Providers} onRestartRequired=${setRestartRequired} />
1131
- </div>
217
+ <${RouteRedirect} to="/models" />
1132
218
  </${Route}>
1133
219
  <${Route} path="/watchdog">
1134
- <div class="pt-4">
1135
- <${WatchdogTab}
1136
- gatewayStatus=${sharedStatus?.gateway || null}
1137
- openclawVersion=${sharedStatus?.openclawVersion || null}
1138
- watchdogStatus=${sharedWatchdogStatus}
1139
- onRefreshStatuses=${refreshSharedStatuses}
1140
- restartingGateway=${restartingGateway}
1141
- onRestartGateway=${handleGatewayRestart}
1142
- restartSignal=${gatewayRestartSignal}
1143
- openclawUpdateInProgress=${openclawUpdateInProgress}
1144
- onOpenclawVersionActionComplete=${handleOpenclawVersionActionComplete}
1145
- onOpenclawUpdate=${handleOpenclawUpdate}
1146
- />
1147
- </div>
220
+ <${WatchdogRoute}
221
+ statusData=${controllerState.sharedStatus}
222
+ watchdogStatus=${controllerState.sharedWatchdogStatus}
223
+ onRefreshStatuses=${controllerActions.refreshSharedStatuses}
224
+ restartingGateway=${controllerState.restartingGateway}
225
+ onRestartGateway=${controllerActions.handleGatewayRestart}
226
+ restartSignal=${controllerState.gatewayRestartSignal}
227
+ openclawUpdateInProgress=${controllerState.openclawUpdateInProgress}
228
+ onOpenclawVersionActionComplete=${controllerActions.handleOpenclawVersionActionComplete}
229
+ onOpenclawUpdate=${controllerActions.handleOpenclawUpdate}
230
+ />
1148
231
  </${Route}>
1149
232
  <${Route} path="/usage/:sessionId">
1150
233
  ${(params) => html`
1151
- <div class="pt-4">
1152
- <${UsageTab}
1153
- sessionId=${decodeURIComponent(params.sessionId || "")}
1154
- onSelectSession=${(id) =>
1155
- setLocation(`/usage/${encodeURIComponent(String(id || ""))}`)}
1156
- onBackToSessions=${() => setLocation("/usage")}
1157
- />
1158
- </div>
234
+ <${UsageRoute}
235
+ sessionId=${decodeURIComponent(params.sessionId || "")}
236
+ onSetLocation=${setLocation}
237
+ />
1159
238
  `}
1160
239
  </${Route}>
1161
240
  <${Route} path="/usage">
1162
- <div class="pt-4">
1163
- <${UsageTab}
1164
- onSelectSession=${(id) =>
1165
- setLocation(`/usage/${encodeURIComponent(String(id || ""))}`)}
1166
- onBackToSessions=${() => setLocation("/usage")}
1167
- />
1168
- </div>
241
+ <${UsageRoute} onSetLocation=${setLocation} />
1169
242
  </${Route}>
1170
243
  <${Route} path="/envars">
1171
- <div class="pt-4">
1172
- <${Envars} onRestartRequired=${setRestartRequired} />
1173
- </div>
244
+ <${EnvarsRoute} onRestartRequired=${controllerActions.setRestartRequired} />
1174
245
  </${Route}>
1175
246
  <${Route} path="/webhooks/:hookName">
1176
- ${(params) =>
1177
- renderWebhooks(decodeURIComponent(params.hookName || ""))}
247
+ ${(params) => html`
248
+ <${WebhooksRoute}
249
+ hookName=${decodeURIComponent(params.hookName || "")}
250
+ routeHistoryRef=${browseState.routeHistoryRef}
251
+ getCurrentPath=${getHashRouterPath}
252
+ onSetLocation=${setLocation}
253
+ onRestartRequired=${controllerActions.setRestartRequired}
254
+ onNavigateToBrowseFile=${browseActions.navigateToBrowseFile}
255
+ />
256
+ `}
1178
257
  </${Route}>
1179
258
  <${Route} path="/webhooks">
1180
- ${() => renderWebhooks("")}
259
+ <${WebhooksRoute}
260
+ routeHistoryRef=${browseState.routeHistoryRef}
261
+ getCurrentPath=${getHashRouterPath}
262
+ onSetLocation=${setLocation}
263
+ onRestartRequired=${controllerActions.setRestartRequired}
264
+ onNavigateToBrowseFile=${browseActions.navigateToBrowseFile}
265
+ />
1181
266
  </${Route}>
1182
267
  <${Route}>
1183
268
  <${RouteRedirect} to="/general" />
@@ -1194,8 +279,8 @@ const App = () => {
1194
279
 
1195
280
  <div class="app-statusbar">
1196
281
  <div class="statusbar-left">
1197
- ${acVersion
1198
- ? html`<span style="color: var(--text-muted)">v${acVersion}</span>`
282
+ ${controllerState.acVersion
283
+ ? html`<span style="color: var(--text-muted)">v${controllerState.acVersion}</span>`
1199
284
  : null}
1200
285
  </div>
1201
286
  <div class="statusbar-right">