@chrysb/alphaclaw 0.3.2 → 0.3.4-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/bin/alphaclaw.js +47 -2
  2. package/lib/cli/git-sync.js +25 -0
  3. package/lib/plugin/usage-tracker/index.js +308 -0
  4. package/lib/plugin/usage-tracker/openclaw.plugin.json +8 -0
  5. package/lib/public/css/explorer.css +1033 -0
  6. package/lib/public/css/shell.css +50 -4
  7. package/lib/public/css/theme.css +41 -1
  8. package/lib/public/icons/folder-line.svg +1 -0
  9. package/lib/public/icons/hashtag.svg +3 -0
  10. package/lib/public/icons/home-5-line.svg +1 -0
  11. package/lib/public/icons/save-fill.svg +3 -0
  12. package/lib/public/js/app.js +310 -160
  13. package/lib/public/js/components/action-button.js +12 -1
  14. package/lib/public/js/components/file-tree.js +497 -0
  15. package/lib/public/js/components/file-viewer.js +714 -0
  16. package/lib/public/js/components/icons.js +182 -0
  17. package/lib/public/js/components/segmented-control.js +33 -0
  18. package/lib/public/js/components/sidebar-git-panel.js +149 -0
  19. package/lib/public/js/components/sidebar.js +254 -0
  20. package/lib/public/js/components/telegram-workspace/index.js +353 -0
  21. package/lib/public/js/components/telegram-workspace/manage.js +397 -0
  22. package/lib/public/js/components/telegram-workspace/onboarding.js +616 -0
  23. package/lib/public/js/components/usage-tab.js +528 -0
  24. package/lib/public/js/components/watchdog-tab.js +1 -1
  25. package/lib/public/js/lib/api.js +51 -1
  26. package/lib/public/js/lib/browse-draft-state.js +109 -0
  27. package/lib/public/js/lib/file-highlighting.js +6 -0
  28. package/lib/public/js/lib/file-tree-utils.js +12 -0
  29. package/lib/public/js/lib/syntax-highlighters/css.js +124 -0
  30. package/lib/public/js/lib/syntax-highlighters/frontmatter.js +49 -0
  31. package/lib/public/js/lib/syntax-highlighters/html.js +209 -0
  32. package/lib/public/js/lib/syntax-highlighters/index.js +28 -0
  33. package/lib/public/js/lib/syntax-highlighters/javascript.js +134 -0
  34. package/lib/public/js/lib/syntax-highlighters/json.js +61 -0
  35. package/lib/public/js/lib/syntax-highlighters/markdown.js +37 -0
  36. package/lib/public/js/lib/syntax-highlighters/utils.js +13 -0
  37. package/lib/public/js/lib/telegram-api.js +78 -0
  38. package/lib/public/js/lib/ui-settings.js +38 -0
  39. package/lib/public/setup.html +34 -29
  40. package/lib/server/alphaclaw-version.js +3 -3
  41. package/lib/server/constants.js +2 -0
  42. package/lib/server/onboarding/openclaw.js +15 -0
  43. package/lib/server/onboarding/workspace.js +3 -2
  44. package/lib/server/routes/auth.js +5 -1
  45. package/lib/server/routes/browse.js +295 -0
  46. package/lib/server/routes/telegram.js +185 -60
  47. package/lib/server/routes/usage.js +133 -0
  48. package/lib/server/usage-db.js +570 -0
  49. package/lib/server.js +45 -4
  50. package/lib/setup/core-prompts/AGENTS.md +0 -101
  51. package/lib/setup/core-prompts/TOOLS.md +3 -1
  52. package/lib/setup/skills/control-ui/SKILL.md +12 -20
  53. package/package.json +1 -1
  54. package/lib/public/js/components/telegram-workspace.js +0 -1365
@@ -34,14 +34,26 @@ import { Welcome } from "./components/welcome.js";
34
34
  import { Envars } from "./components/envars.js";
35
35
  import { Webhooks } from "./components/webhooks.js";
36
36
  import { ToastContainer, showToast } from "./components/toast.js";
37
- import { TelegramWorkspace } from "./components/telegram-workspace.js";
37
+ import { TelegramWorkspace } from "./components/telegram-workspace/index.js";
38
38
  import { ChevronDownIcon } from "./components/icons.js";
39
39
  import { UpdateActionButton } from "./components/update-action-button.js";
40
40
  import { GlobalRestartBanner } from "./components/global-restart-banner.js";
41
41
  import { LoadingSpinner } from "./components/loading-spinner.js";
42
42
  import { WatchdogTab } from "./components/watchdog-tab.js";
43
+ import { FileViewer } from "./components/file-viewer.js";
44
+ import { AppSidebar } from "./components/sidebar.js";
45
+ import { UsageTab } from "./components/usage-tab.js";
46
+ import { readUiSettings, writeUiSettings } from "./lib/ui-settings.js";
43
47
  const html = htm.bind(h);
44
48
  const kDefaultUiTab = "general";
49
+ const kDefaultSidebarWidthPx = 220;
50
+ const kSidebarMinWidthPx = 180;
51
+ const kSidebarMaxWidthPx = 460;
52
+ const kBrowseLastPathUiSettingKey = "browseLastPath";
53
+ const kLastMenuRouteUiSettingKey = "lastMenuRoute";
54
+
55
+ const clampSidebarWidth = (value) =>
56
+ Math.max(kSidebarMinWidthPx, Math.min(kSidebarMaxWidthPx, value));
45
57
 
46
58
  const getHashPath = () => {
47
59
  const hash = window.location.hash.replace(/^#/, "");
@@ -158,16 +170,12 @@ const GeneralTab = ({
158
170
  setTimeout(devicePoll.refresh, 2000);
159
171
  };
160
172
 
161
- const fullRefresh = () => {
173
+ useEffect(() => {
174
+ if (!isActive) return;
162
175
  onRefreshStatuses();
163
176
  pairingsPoll.refresh();
164
177
  devicePoll.refresh();
165
178
  setGoogleKey((k) => k + 1);
166
- };
167
-
168
- useEffect(() => {
169
- if (!isActive) return;
170
- fullRefresh();
171
179
  }, [isActive]);
172
180
 
173
181
  useEffect(() => {
@@ -360,23 +368,12 @@ const GeneralTab = ({
360
368
  onReject=${handleDeviceReject}
361
369
  />
362
370
  </div>
363
-
364
- <p class="text-center text-gray-600 text-xs">
365
- <a
366
- href="#"
367
- onclick=${(e) => {
368
- e.preventDefault();
369
- fullRefresh();
370
- }}
371
- class="text-gray-500 hover:text-gray-300"
372
- >Refresh all</a
373
- >
374
- </p>
375
371
  </div>
376
372
  `;
377
373
  };
378
374
 
379
375
  const App = () => {
376
+ const appShellRef = useRef(null);
380
377
  const [onboarded, setOnboarded] = useState(null);
381
378
  const [location, setLocation] = useLocation();
382
379
  const [acVersion, setAcVersion] = useState(null);
@@ -386,6 +383,32 @@ const App = () => {
386
383
  const [acDismissed, setAcDismissed] = useState(false);
387
384
  const [authEnabled, setAuthEnabled] = useState(false);
388
385
  const [menuOpen, setMenuOpen] = useState(false);
386
+ const [sidebarTab, setSidebarTab] = useState("menu");
387
+ const [sidebarWidthPx, setSidebarWidthPx] = useState(() => {
388
+ const settings = readUiSettings();
389
+ if (!Number.isFinite(settings.sidebarWidthPx)) return kDefaultSidebarWidthPx;
390
+ return clampSidebarWidth(settings.sidebarWidthPx);
391
+ });
392
+ const [lastBrowsePath, setLastBrowsePath] = useState(() => {
393
+ const settings = readUiSettings();
394
+ return typeof settings[kBrowseLastPathUiSettingKey] === "string"
395
+ ? settings[kBrowseLastPathUiSettingKey]
396
+ : "";
397
+ });
398
+ const [lastMenuRoute, setLastMenuRoute] = useState(() => {
399
+ const settings = readUiSettings();
400
+ const storedRoute = settings[kLastMenuRouteUiSettingKey];
401
+ if (
402
+ typeof storedRoute === "string" &&
403
+ storedRoute.startsWith("/") &&
404
+ !storedRoute.startsWith("/browse")
405
+ ) {
406
+ return storedRoute;
407
+ }
408
+ return `/${kDefaultUiTab}`;
409
+ });
410
+ const [isResizingSidebar, setIsResizingSidebar] = useState(false);
411
+ const [browsePreviewPath, setBrowsePreviewPath] = useState("");
389
412
  const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
390
413
  const [mobileTopbarScrolled, setMobileTopbarScrolled] = useState(false);
391
414
  const [restartRequired, setRestartRequired] = useState(false);
@@ -568,10 +591,47 @@ const App = () => {
568
591
  `;
569
592
  }
570
593
 
594
+ const buildBrowseRoute = (relativePath) => {
595
+ const encodedPath = String(relativePath || "")
596
+ .split("/")
597
+ .filter(Boolean)
598
+ .map((segment) => encodeURIComponent(segment))
599
+ .join("/");
600
+ return encodedPath ? `/browse/${encodedPath}` : "/browse";
601
+ };
571
602
  const navigateToSubScreen = (screen) => {
572
603
  setLocation(`/${screen}`);
573
604
  setMobileSidebarOpen(false);
574
605
  };
606
+ const navigateToBrowseFile = (relativePath) => {
607
+ setBrowsePreviewPath("");
608
+ setLocation(buildBrowseRoute(relativePath));
609
+ setMobileSidebarOpen(false);
610
+ };
611
+ const handleSidebarLogout = async () => {
612
+ setMenuOpen(false);
613
+ await logout();
614
+ try {
615
+ window.localStorage.clear();
616
+ window.sessionStorage.clear();
617
+ } catch {}
618
+ window.location.href = "/login.html";
619
+ };
620
+ const handleSelectSidebarTab = (nextTab) => {
621
+ setSidebarTab(nextTab);
622
+ if (nextTab === "menu" && location.startsWith("/browse")) {
623
+ setBrowsePreviewPath("");
624
+ setLocation(lastMenuRoute || `/${kDefaultUiTab}`);
625
+ return;
626
+ }
627
+ if (nextTab === "browse" && !location.startsWith("/browse")) {
628
+ setLocation(buildBrowseRoute(lastBrowsePath));
629
+ }
630
+ };
631
+ const handleSelectNavItem = (itemId) => {
632
+ setLocation(`/${itemId}`);
633
+ setMobileSidebarOpen(false);
634
+ };
575
635
  const exitSubScreen = () => {
576
636
  setLocation(`/${kDefaultUiTab}`);
577
637
  setMobileSidebarOpen(false);
@@ -585,10 +645,16 @@ const App = () => {
585
645
 
586
646
  const kNavSections = [
587
647
  {
588
- label: "Status",
648
+ label: "Setup",
589
649
  items: [
590
650
  { id: "general", label: "General" },
651
+ ],
652
+ },
653
+ {
654
+ label: "Monitoring",
655
+ items: [
591
656
  { id: "watchdog", label: "Watchdog" },
657
+ { id: "usage", label: "Usage" },
592
658
  ],
593
659
  },
594
660
  {
@@ -601,18 +667,105 @@ const App = () => {
601
667
  },
602
668
  ];
603
669
 
604
- const selectedNavId =
605
- location === "/telegram"
606
- ? ""
607
- : location.startsWith("/providers")
608
- ? "providers"
609
- : location.startsWith("/watchdog")
610
- ? "watchdog"
611
- : location.startsWith("/envars")
612
- ? "envars"
613
- : location.startsWith("/webhooks")
614
- ? "webhooks"
615
- : "general";
670
+ const isBrowseRoute = location.startsWith("/browse");
671
+ const selectedBrowsePath = isBrowseRoute
672
+ ? location
673
+ .replace(/^\/browse\/?/, "")
674
+ .split("/")
675
+ .filter(Boolean)
676
+ .map((segment) => {
677
+ try {
678
+ return decodeURIComponent(segment);
679
+ } catch {
680
+ return segment;
681
+ }
682
+ })
683
+ .join("/")
684
+ : "";
685
+ const selectedNavId = isBrowseRoute
686
+ ? "browse"
687
+ : location === "/telegram"
688
+ ? ""
689
+ : location.startsWith("/providers")
690
+ ? "providers"
691
+ : location.startsWith("/watchdog")
692
+ ? "watchdog"
693
+ : location.startsWith("/usage")
694
+ ? "usage"
695
+ : location.startsWith("/envars")
696
+ ? "envars"
697
+ : location.startsWith("/webhooks")
698
+ ? "webhooks"
699
+ : "general";
700
+
701
+ useEffect(() => {
702
+ setSidebarTab((currentTab) => {
703
+ if (location.startsWith("/browse")) return "browse";
704
+ if (currentTab === "browse") return "menu";
705
+ return currentTab;
706
+ });
707
+ }, [location]);
708
+
709
+ useEffect(() => {
710
+ if (location.startsWith("/browse")) return;
711
+ setBrowsePreviewPath("");
712
+ }, [location]);
713
+
714
+ useEffect(() => {
715
+ if (location.startsWith("/browse")) return;
716
+ if (location === "/telegram") return;
717
+ setLastMenuRoute((currentRoute) =>
718
+ currentRoute === location ? currentRoute : location,
719
+ );
720
+ }, [location]);
721
+
722
+ useEffect(() => {
723
+ if (!isBrowseRoute) return;
724
+ if (!selectedBrowsePath) return;
725
+ setLastBrowsePath((currentPath) =>
726
+ currentPath === selectedBrowsePath ? currentPath : selectedBrowsePath,
727
+ );
728
+ }, [isBrowseRoute, selectedBrowsePath]);
729
+
730
+ useEffect(() => {
731
+ const settings = readUiSettings();
732
+ settings.sidebarWidthPx = sidebarWidthPx;
733
+ settings[kBrowseLastPathUiSettingKey] = lastBrowsePath;
734
+ settings[kLastMenuRouteUiSettingKey] = lastMenuRoute;
735
+ writeUiSettings(settings);
736
+ }, [sidebarWidthPx, lastBrowsePath, lastMenuRoute]);
737
+
738
+ const resizeSidebarWithClientX = useCallback((clientX) => {
739
+ const shellElement = appShellRef.current;
740
+ if (!shellElement) return;
741
+ const shellBounds = shellElement.getBoundingClientRect();
742
+ const nextWidth = clampSidebarWidth(Math.round(clientX - shellBounds.left));
743
+ setSidebarWidthPx(nextWidth);
744
+ }, []);
745
+
746
+ const onSidebarResizerPointerDown = (event) => {
747
+ event.preventDefault();
748
+ setIsResizingSidebar(true);
749
+ resizeSidebarWithClientX(event.clientX);
750
+ };
751
+
752
+ useEffect(() => {
753
+ if (!isResizingSidebar) return () => {};
754
+ const onPointerMove = (event) => resizeSidebarWithClientX(event.clientX);
755
+ const onPointerUp = () => setIsResizingSidebar(false);
756
+ window.addEventListener("pointermove", onPointerMove);
757
+ window.addEventListener("pointerup", onPointerUp);
758
+ const previousUserSelect = document.body.style.userSelect;
759
+ const previousCursor = document.body.style.cursor;
760
+ document.body.style.userSelect = "none";
761
+ document.body.style.cursor = "col-resize";
762
+ return () => {
763
+ window.removeEventListener("pointermove", onPointerMove);
764
+ window.removeEventListener("pointerup", onPointerUp);
765
+ document.body.style.userSelect = previousUserSelect;
766
+ document.body.style.cursor = previousCursor;
767
+ };
768
+ }, [isResizingSidebar, resizeSidebarWithClientX]);
616
769
 
617
770
  const renderWebhooks = (hookName = "") => html`
618
771
  <div class="pt-4">
@@ -626,91 +779,54 @@ const App = () => {
626
779
  `;
627
780
 
628
781
  return html`
629
- <div class="app-shell">
782
+ <div
783
+ class="app-shell"
784
+ ref=${appShellRef}
785
+ style=${{ "--sidebar-width": `${sidebarWidthPx}px` }}
786
+ >
630
787
  <${GlobalRestartBanner}
631
788
  visible=${restartRequired}
632
789
  restarting=${restartingGateway}
633
790
  onRestart=${handleGatewayRestart}
634
791
  />
635
- <div class=${`app-sidebar ${mobileSidebarOpen ? "mobile-open" : ""}`}>
636
- <div class="sidebar-brand">
637
- <img src="./img/logo.svg" alt="" width="20" height="20" />
638
- <span><span style="color: var(--accent)">alpha</span>claw</span>
639
- ${authEnabled && html`
640
- <div class="brand-menu" ref=${menuRef}>
641
- <button
642
- class="brand-menu-trigger"
643
- onclick=${() => setMenuOpen((o) => !o)}
644
- aria-label="Menu"
645
- >
646
- <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
647
- <circle cx="8" cy="3" r="1.5" />
648
- <circle cx="8" cy="8" r="1.5" />
649
- <circle cx="8" cy="13" r="1.5" />
650
- </svg>
651
- </button>
652
- ${menuOpen && html`
653
- <div class="brand-dropdown">
654
- <a
655
- href="#"
656
- onclick=${async (e) => {
657
- e.preventDefault();
658
- setMenuOpen(false);
659
- await logout();
660
- try {
661
- window.localStorage.clear();
662
- window.sessionStorage.clear();
663
- } catch {}
664
- window.location.href = "/login.html";
665
- }}
666
- >Log out</a>
667
- </div>
668
- `}
669
- </div>
670
- `}
671
- </div>
672
- ${kNavSections.map(
673
- (section) => html`
674
- <div class="sidebar-label">${section.label}</div>
675
- <nav class="sidebar-nav">
676
- ${section.items.map(
677
- (item) => html`
678
- <a
679
- class=${selectedNavId === item.id ? "active" : ""}
680
- onclick=${() => {
681
- setLocation(`/${item.id}`);
682
- setMobileSidebarOpen(false);
683
- }}
684
- >
685
- ${item.label}
686
- </a>
687
- `,
688
- )}
689
- </nav>
690
- `,
691
- )}
692
- <div class="sidebar-footer">
693
- ${acHasUpdate && acLatest && !acDismissed
694
- ? html`
695
- <${UpdateActionButton}
696
- onClick=${handleAcUpdate}
697
- loading=${acUpdating}
698
- warning=${true}
699
- idleLabel=${`Update to v${acLatest}`}
700
- loadingLabel="Updating..."
701
- className="w-full justify-center"
702
- />
703
- `
704
- : null}
705
- </div>
706
- </div>
792
+ <${AppSidebar}
793
+ mobileSidebarOpen=${mobileSidebarOpen}
794
+ authEnabled=${authEnabled}
795
+ menuRef=${menuRef}
796
+ menuOpen=${menuOpen}
797
+ onToggleMenu=${() => setMenuOpen((open) => !open)}
798
+ onLogout=${handleSidebarLogout}
799
+ sidebarTab=${sidebarTab}
800
+ onSelectSidebarTab=${handleSelectSidebarTab}
801
+ navSections=${kNavSections}
802
+ selectedNavId=${selectedNavId}
803
+ onSelectNavItem=${handleSelectNavItem}
804
+ selectedBrowsePath=${selectedBrowsePath}
805
+ onSelectBrowseFile=${navigateToBrowseFile}
806
+ onPreviewBrowseFile=${setBrowsePreviewPath}
807
+ acHasUpdate=${acHasUpdate}
808
+ acLatest=${acLatest}
809
+ acDismissed=${acDismissed}
810
+ acUpdating=${acUpdating}
811
+ onAcUpdate=${handleAcUpdate}
812
+ />
813
+ <div
814
+ class=${`sidebar-resizer ${isResizingSidebar ? "is-resizing" : ""}`}
815
+ onpointerdown=${onSidebarResizerPointerDown}
816
+ role="separator"
817
+ aria-orientation="vertical"
818
+ aria-label="Resize sidebar"
819
+ ></div>
707
820
 
708
821
  <div
709
822
  class=${`mobile-sidebar-overlay ${mobileSidebarOpen ? "active" : ""}`}
710
823
  onclick=${() => setMobileSidebarOpen(false)}
711
824
  />
712
825
 
713
- <div class="app-content" onscroll=${handleAppContentScroll}>
826
+ <div
827
+ class=${`app-content ${isBrowseRoute ? "browse-mode" : ""}`}
828
+ onscroll=${handleAppContentScroll}
829
+ >
714
830
  <div class=${`mobile-topbar ${mobileTopbarScrolled ? "is-scrolled" : ""}`}>
715
831
  <button
716
832
  class="mobile-topbar-menu"
@@ -728,61 +844,95 @@ const App = () => {
728
844
  <span style="color: var(--accent)">alpha</span>claw
729
845
  </span>
730
846
  </div>
731
- <div class="max-w-2xl w-full mx-auto">
732
- <${Switch}>
733
- <${Route} path="/telegram">
734
- <div class="pt-4">
735
- <${TelegramWorkspace} onBack=${exitSubScreen} />
736
- </div>
737
- </${Route}>
738
- <${Route} path="/general">
739
- <div class="pt-4">
740
- <${GeneralTab}
741
- statusData=${sharedStatus}
742
- watchdogData=${sharedWatchdogStatus}
743
- onRefreshStatuses=${refreshSharedStatuses}
744
- onSwitchTab=${(nextTab) => setLocation(`/${nextTab}`)}
745
- onNavigate=${navigateToSubScreen}
746
- isActive=${location === "/general"}
747
- restartingGateway=${restartingGateway}
748
- onRestartGateway=${handleGatewayRestart}
749
- restartSignal=${gatewayRestartSignal}
750
- />
751
- </div>
752
- </${Route}>
753
- <${Route} path="/providers">
754
- <div class="pt-4">
755
- <${Providers} onRestartRequired=${setRestartRequired} />
756
- </div>
757
- </${Route}>
758
- <${Route} path="/watchdog">
759
- <div class="pt-4">
760
- <${WatchdogTab}
761
- gatewayStatus=${sharedStatus?.gateway || null}
762
- openclawVersion=${sharedStatus?.openclawVersion || null}
763
- watchdogStatus=${sharedWatchdogStatus}
764
- onRefreshStatuses=${refreshSharedStatuses}
765
- restartingGateway=${restartingGateway}
766
- onRestartGateway=${handleGatewayRestart}
767
- restartSignal=${gatewayRestartSignal}
847
+ <div class=${isBrowseRoute ? "w-full" : "max-w-2xl w-full mx-auto"}>
848
+ ${isBrowseRoute
849
+ ? html`
850
+ <${FileViewer}
851
+ filePath=${browsePreviewPath || selectedBrowsePath}
852
+ isPreviewOnly=${Boolean(
853
+ browsePreviewPath &&
854
+ browsePreviewPath !== selectedBrowsePath,
855
+ )}
768
856
  />
769
- </div>
770
- </${Route}>
771
- <${Route} path="/envars">
772
- <div class="pt-4">
773
- <${Envars} onRestartRequired=${setRestartRequired} />
774
- </div>
775
- </${Route}>
776
- <${Route} path="/webhooks/:hookName">
777
- ${(params) => renderWebhooks(decodeURIComponent(params.hookName || ""))}
778
- </${Route}>
779
- <${Route} path="/webhooks">
780
- ${() => renderWebhooks("")}
781
- </${Route}>
782
- <${Route}>
783
- <${RouteRedirect} to="/general" />
784
- </${Route}>
785
- </${Switch}>
857
+ `
858
+ : html`
859
+ <${Switch}>
860
+ <${Route} path="/telegram">
861
+ <div class="pt-4">
862
+ <${TelegramWorkspace} onBack=${exitSubScreen} />
863
+ </div>
864
+ </${Route}>
865
+ <${Route} path="/general">
866
+ <div class="pt-4">
867
+ <${GeneralTab}
868
+ statusData=${sharedStatus}
869
+ watchdogData=${sharedWatchdogStatus}
870
+ onRefreshStatuses=${refreshSharedStatuses}
871
+ onSwitchTab=${(nextTab) => setLocation(`/${nextTab}`)}
872
+ onNavigate=${navigateToSubScreen}
873
+ isActive=${location === "/general"}
874
+ restartingGateway=${restartingGateway}
875
+ onRestartGateway=${handleGatewayRestart}
876
+ restartSignal=${gatewayRestartSignal}
877
+ />
878
+ </div>
879
+ </${Route}>
880
+ <${Route} path="/providers">
881
+ <div class="pt-4">
882
+ <${Providers} onRestartRequired=${setRestartRequired} />
883
+ </div>
884
+ </${Route}>
885
+ <${Route} path="/watchdog">
886
+ <div class="pt-4">
887
+ <${WatchdogTab}
888
+ gatewayStatus=${sharedStatus?.gateway || null}
889
+ openclawVersion=${sharedStatus?.openclawVersion || null}
890
+ watchdogStatus=${sharedWatchdogStatus}
891
+ onRefreshStatuses=${refreshSharedStatuses}
892
+ restartingGateway=${restartingGateway}
893
+ onRestartGateway=${handleGatewayRestart}
894
+ restartSignal=${gatewayRestartSignal}
895
+ />
896
+ </div>
897
+ </${Route}>
898
+ <${Route} path="/usage/:sessionId">
899
+ ${(params) => html`
900
+ <div class="pt-4">
901
+ <${UsageTab}
902
+ sessionId=${decodeURIComponent(params.sessionId || "")}
903
+ onSelectSession=${(id) =>
904
+ setLocation(`/usage/${encodeURIComponent(String(id || ""))}`)}
905
+ onBackToSessions=${() => setLocation("/usage")}
906
+ />
907
+ </div>
908
+ `}
909
+ </${Route}>
910
+ <${Route} path="/usage">
911
+ <div class="pt-4">
912
+ <${UsageTab}
913
+ onSelectSession=${(id) =>
914
+ setLocation(`/usage/${encodeURIComponent(String(id || ""))}`)}
915
+ onBackToSessions=${() => setLocation("/usage")}
916
+ />
917
+ </div>
918
+ </${Route}>
919
+ <${Route} path="/envars">
920
+ <div class="pt-4">
921
+ <${Envars} onRestartRequired=${setRestartRequired} />
922
+ </div>
923
+ </${Route}>
924
+ <${Route} path="/webhooks/:hookName">
925
+ ${(params) =>
926
+ renderWebhooks(decodeURIComponent(params.hookName || ""))}
927
+ </${Route}>
928
+ <${Route} path="/webhooks">
929
+ ${() => renderWebhooks("")}
930
+ </${Route}>
931
+ <${Route}>
932
+ <${RouteRedirect} to="/general" />
933
+ </${Route}>
934
+ </${Switch}>
935
+ `}
786
936
  </div>
787
937
  <${ToastContainer}
788
938
  className="fixed bottom-10 right-4 z-50 space-y-2 pointer-events-none"
@@ -43,6 +43,8 @@ export const ActionButton = ({
43
43
  loadingLabel = "Working...",
44
44
  loadingMode = "replace",
45
45
  className = "",
46
+ idleIcon = null,
47
+ idleIconClassName = "h-3 w-3",
46
48
  }) => {
47
49
  const isDisabled = disabled || loading;
48
50
  const isInteractive = !isDisabled;
@@ -57,7 +59,16 @@ export const ActionButton = ({
57
59
  : "";
58
60
  const spinnerSizeClass = size === "md" || size === "lg" ? "h-4 w-4" : "h-3 w-3";
59
61
  const isInlineLoading = loadingMode === "inline";
60
- const currentLabel = loading && !isInlineLoading ? loadingLabel : idleLabel;
62
+ const IdleIcon = idleIcon;
63
+ const idleContent = IdleIcon
64
+ ? html`
65
+ <span class="inline-flex items-center gap-1.5 leading-none">
66
+ <${IdleIcon} className=${idleIconClassName} />
67
+ ${idleLabel}
68
+ </span>
69
+ `
70
+ : idleLabel;
71
+ const currentLabel = loading && !isInlineLoading ? loadingLabel : idleContent;
61
72
 
62
73
  return html`
63
74
  <button