@chrysb/alphaclaw 0.3.2 → 0.3.3

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 (36) hide show
  1. package/bin/alphaclaw.js +29 -2
  2. package/lib/cli/git-sync.js +25 -0
  3. package/lib/public/css/explorer.css +983 -0
  4. package/lib/public/css/shell.css +48 -4
  5. package/lib/public/css/theme.css +6 -1
  6. package/lib/public/icons/folder-line.svg +1 -0
  7. package/lib/public/icons/hashtag.svg +3 -0
  8. package/lib/public/icons/home-5-line.svg +1 -0
  9. package/lib/public/icons/save-fill.svg +3 -0
  10. package/lib/public/js/app.js +259 -158
  11. package/lib/public/js/components/action-button.js +12 -1
  12. package/lib/public/js/components/file-tree.js +322 -0
  13. package/lib/public/js/components/file-viewer.js +691 -0
  14. package/lib/public/js/components/icons.js +182 -0
  15. package/lib/public/js/components/sidebar-git-panel.js +149 -0
  16. package/lib/public/js/components/sidebar.js +272 -0
  17. package/lib/public/js/lib/api.js +26 -0
  18. package/lib/public/js/lib/browse-draft-state.js +109 -0
  19. package/lib/public/js/lib/file-highlighting.js +6 -0
  20. package/lib/public/js/lib/file-tree-utils.js +12 -0
  21. package/lib/public/js/lib/syntax-highlighters/css.js +124 -0
  22. package/lib/public/js/lib/syntax-highlighters/frontmatter.js +49 -0
  23. package/lib/public/js/lib/syntax-highlighters/html.js +209 -0
  24. package/lib/public/js/lib/syntax-highlighters/index.js +28 -0
  25. package/lib/public/js/lib/syntax-highlighters/javascript.js +134 -0
  26. package/lib/public/js/lib/syntax-highlighters/json.js +61 -0
  27. package/lib/public/js/lib/syntax-highlighters/markdown.js +37 -0
  28. package/lib/public/js/lib/syntax-highlighters/utils.js +13 -0
  29. package/lib/public/setup.html +1 -0
  30. package/lib/server/constants.js +1 -0
  31. package/lib/server/onboarding/workspace.js +3 -2
  32. package/lib/server/routes/browse.js +295 -0
  33. package/lib/server.js +24 -3
  34. package/lib/setup/core-prompts/TOOLS.md +3 -1
  35. package/lib/setup/skills/control-ui/SKILL.md +12 -20
  36. package/package.json +1 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  .app-shell {
4
4
  display: grid;
5
- grid-template-columns: 220px 1fr;
5
+ grid-template-columns: 220px 0 minmax(0, 1fr);
6
6
  grid-template-rows: auto 1fr 24px;
7
7
  height: 100vh;
8
8
  position: relative;
@@ -42,6 +42,7 @@
42
42
  }
43
43
 
44
44
  .app-content {
45
+ grid-column: 3;
45
46
  grid-row: 2;
46
47
  overflow-y: auto;
47
48
  padding: 24px 32px;
@@ -52,16 +53,54 @@
52
53
  /* ── Sidebar ───────────────────────────────────── */
53
54
 
54
55
  .app-sidebar {
56
+ grid-column: 1;
55
57
  grid-row: 2;
56
58
  background:
57
- linear-gradient(180deg, rgba(255, 255, 255, 0.03) 0%, rgba(255, 255, 255, 0.012) 100%),
59
+ linear-gradient(180deg, rgba(255, 255, 255, 0.018) 0%, rgba(255, 255, 255, 0.006) 100%),
60
+ linear-gradient(180deg, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.16) 100%),
58
61
  var(--bg-sidebar);
59
- border-right: 1px solid var(--border-strong);
62
+ border-right: 1px solid var(--border);
60
63
  overflow-y: auto;
61
64
  display: flex;
62
65
  flex-direction: column;
63
66
  }
64
67
 
68
+ .sidebar-resizer {
69
+ grid-column: 2;
70
+ grid-row: 2;
71
+ cursor: col-resize;
72
+ position: relative;
73
+ width: 6px;
74
+ margin-left: -3px;
75
+ z-index: 4;
76
+ }
77
+
78
+ .sidebar-resizer::before {
79
+ content: "";
80
+ position: absolute;
81
+ left: 0;
82
+ top: 0;
83
+ bottom: 0;
84
+ width: 6px;
85
+ background: transparent;
86
+ }
87
+
88
+ .sidebar-resizer::after {
89
+ content: "";
90
+ position: absolute;
91
+ left: 2px;
92
+ top: 0;
93
+ bottom: 0;
94
+ width: 2px;
95
+ background: transparent;
96
+ transition: background 0.12s;
97
+ }
98
+
99
+ .sidebar-resizer:hover::after,
100
+ .sidebar-resizer.is-resizing::after {
101
+ background: rgba(99, 235, 255, 0.55);
102
+ }
103
+
65
104
  .sidebar-brand {
66
105
  padding: 16px;
67
106
  font-size: 14px;
@@ -122,7 +161,7 @@
122
161
  .sidebar-footer:empty { display: none; }
123
162
 
124
163
  .sidebar-footer:not(:empty) {
125
- padding: 12px 16px;
164
+ padding: 0 16px 12px 16px;
126
165
  border-top: 1px solid var(--border);
127
166
  }
128
167
 
@@ -267,10 +306,15 @@
267
306
  transform: none;
268
307
  }
269
308
  .app-content {
309
+ grid-column: 1;
270
310
  grid-row: 2;
271
311
  padding: 0 14px 12px;
272
312
  }
273
313
 
314
+ .sidebar-resizer {
315
+ display: none;
316
+ }
317
+
274
318
  .mobile-topbar {
275
319
  display: flex;
276
320
  align-items: center;
@@ -1,6 +1,6 @@
1
1
  :root {
2
2
  --bg: #0d121b;
3
- --bg-sidebar: #111826;
3
+ --bg-sidebar: #0f141f;
4
4
  --bg-content: #0f1521;
5
5
  --bg-hover: rgba(99, 235, 255, 0.05);
6
6
  --bg-active: rgba(99, 235, 255, 0.08);
@@ -12,6 +12,11 @@
12
12
  --accent: #63ebff;
13
13
  --accent-dim: rgba(99, 235, 255, 0.4);
14
14
  --accent-link: rgba(99, 235, 255, 0.6);
15
+ --orange: #d98a58;
16
+ --comment: #6a737d;
17
+ --keyword: #ff7b72;
18
+ --string: #a5d6ff;
19
+ --number: #79c0ff;
15
20
  --panel-bg-contrast: rgba(255, 255, 255, 0.028);
16
21
  --panel-border-contrast: rgba(255, 255, 255, 0.11);
17
22
  --field-bg-contrast: rgba(0, 0, 0, 0.3);
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M4 5V19H20V7H11.5858L9.58579 5H4ZM12.4142 5H21C21.5523 5 22 5.44772 22 6V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H10.4142L12.4142 5Z"></path></svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
2
+ <path d="M7.78428 14L8.2047 10H4V8H8.41491L8.94043 3H10.9514L10.4259 8H14.4149L14.9404 3H16.9514L16.4259 8H20V10H16.2157L15.7953 14H20V16H15.5851L15.0596 21H13.0486L13.5741 16H9.58509L9.05957 21H7.04855L7.57407 16H4V14H7.78428ZM9.7953 14H13.7843L14.2047 10H10.2157L9.7953 14Z"></path>
3
+ </svg>
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M13 19H19V9.97815L12 4.53371L5 9.97815V19H11V13H13V19ZM21 20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V9.48907C3 9.18048 3.14247 8.88917 3.38606 8.69972L11.3861 2.47749C11.7472 2.19663 12.2528 2.19663 12.6139 2.47749L20.6139 8.69972C20.8575 8.88917 21 9.18048 21 9.48907V20Z"></path></svg>
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
2
+ <path d="M18 21V13H6V21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3H17L21 7V20C21 20.5523 20.5523 21 20 21H18ZM16 21H8V15H16V21Z"></path>
3
+ </svg>
@@ -40,8 +40,32 @@ 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";
43
45
  const html = htm.bind(h);
44
46
  const kDefaultUiTab = "general";
47
+ const kUiSettingsStorageKey = "alphaclaw.uiSettings";
48
+ const kLegacyUiSettingsStorageKey = "alphaclawUiSettings";
49
+ const kDefaultSidebarWidthPx = 220;
50
+ const kSidebarMinWidthPx = 180;
51
+ const kSidebarMaxWidthPx = 460;
52
+ const kBrowseLastPathUiSettingKey = "browseLastPath";
53
+
54
+ const clampSidebarWidth = (value) =>
55
+ Math.max(kSidebarMinWidthPx, Math.min(kSidebarMaxWidthPx, value));
56
+
57
+ const readUiSettings = () => {
58
+ try {
59
+ const raw =
60
+ window.localStorage.getItem(kUiSettingsStorageKey) ||
61
+ window.localStorage.getItem(kLegacyUiSettingsStorageKey);
62
+ if (!raw) return {};
63
+ const parsed = JSON.parse(raw);
64
+ return parsed && typeof parsed === "object" ? parsed : {};
65
+ } catch {
66
+ return {};
67
+ }
68
+ };
45
69
 
46
70
  const getHashPath = () => {
47
71
  const hash = window.location.hash.replace(/^#/, "");
@@ -158,16 +182,12 @@ const GeneralTab = ({
158
182
  setTimeout(devicePoll.refresh, 2000);
159
183
  };
160
184
 
161
- const fullRefresh = () => {
185
+ useEffect(() => {
186
+ if (!isActive) return;
162
187
  onRefreshStatuses();
163
188
  pairingsPoll.refresh();
164
189
  devicePoll.refresh();
165
190
  setGoogleKey((k) => k + 1);
166
- };
167
-
168
- useEffect(() => {
169
- if (!isActive) return;
170
- fullRefresh();
171
191
  }, [isActive]);
172
192
 
173
193
  useEffect(() => {
@@ -360,23 +380,12 @@ const GeneralTab = ({
360
380
  onReject=${handleDeviceReject}
361
381
  />
362
382
  </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
383
  </div>
376
384
  `;
377
385
  };
378
386
 
379
387
  const App = () => {
388
+ const appShellRef = useRef(null);
380
389
  const [onboarded, setOnboarded] = useState(null);
381
390
  const [location, setLocation] = useLocation();
382
391
  const [acVersion, setAcVersion] = useState(null);
@@ -386,6 +395,19 @@ const App = () => {
386
395
  const [acDismissed, setAcDismissed] = useState(false);
387
396
  const [authEnabled, setAuthEnabled] = useState(false);
388
397
  const [menuOpen, setMenuOpen] = useState(false);
398
+ const [sidebarTab, setSidebarTab] = useState("menu");
399
+ const [sidebarWidthPx, setSidebarWidthPx] = useState(() => {
400
+ const settings = readUiSettings();
401
+ if (!Number.isFinite(settings.sidebarWidthPx)) return kDefaultSidebarWidthPx;
402
+ return clampSidebarWidth(settings.sidebarWidthPx);
403
+ });
404
+ const [lastBrowsePath, setLastBrowsePath] = useState(() => {
405
+ const settings = readUiSettings();
406
+ return typeof settings[kBrowseLastPathUiSettingKey] === "string"
407
+ ? settings[kBrowseLastPathUiSettingKey]
408
+ : "";
409
+ });
410
+ const [isResizingSidebar, setIsResizingSidebar] = useState(false);
389
411
  const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
390
412
  const [mobileTopbarScrolled, setMobileTopbarScrolled] = useState(false);
391
413
  const [restartRequired, setRestartRequired] = useState(false);
@@ -568,10 +590,45 @@ const App = () => {
568
590
  `;
569
591
  }
570
592
 
593
+ const buildBrowseRoute = (relativePath) => {
594
+ const encodedPath = String(relativePath || "")
595
+ .split("/")
596
+ .filter(Boolean)
597
+ .map((segment) => encodeURIComponent(segment))
598
+ .join("/");
599
+ return encodedPath ? `/browse/${encodedPath}` : "/browse";
600
+ };
571
601
  const navigateToSubScreen = (screen) => {
572
602
  setLocation(`/${screen}`);
573
603
  setMobileSidebarOpen(false);
574
604
  };
605
+ const navigateToBrowseFile = (relativePath) => {
606
+ setLocation(buildBrowseRoute(relativePath));
607
+ setMobileSidebarOpen(false);
608
+ };
609
+ const handleSidebarLogout = async () => {
610
+ setMenuOpen(false);
611
+ await logout();
612
+ try {
613
+ window.localStorage.clear();
614
+ window.sessionStorage.clear();
615
+ } catch {}
616
+ window.location.href = "/login.html";
617
+ };
618
+ const handleSelectSidebarTab = (nextTab) => {
619
+ setSidebarTab(nextTab);
620
+ if (nextTab === "menu" && location.startsWith("/browse")) {
621
+ setLocation("/general");
622
+ return;
623
+ }
624
+ if (nextTab === "browse" && !location.startsWith("/browse")) {
625
+ setLocation(buildBrowseRoute(lastBrowsePath));
626
+ }
627
+ };
628
+ const handleSelectNavItem = (itemId) => {
629
+ setLocation(`/${itemId}`);
630
+ setMobileSidebarOpen(false);
631
+ };
575
632
  const exitSubScreen = () => {
576
633
  setLocation(`/${kDefaultUiTab}`);
577
634
  setMobileSidebarOpen(false);
@@ -601,18 +658,91 @@ const App = () => {
601
658
  },
602
659
  ];
603
660
 
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";
661
+ const isBrowseRoute = location.startsWith("/browse");
662
+ const selectedBrowsePath = isBrowseRoute
663
+ ? location
664
+ .replace(/^\/browse\/?/, "")
665
+ .split("/")
666
+ .filter(Boolean)
667
+ .map((segment) => {
668
+ try {
669
+ return decodeURIComponent(segment);
670
+ } catch {
671
+ return segment;
672
+ }
673
+ })
674
+ .join("/")
675
+ : "";
676
+ const selectedNavId = isBrowseRoute
677
+ ? "browse"
678
+ : location === "/telegram"
679
+ ? ""
680
+ : location.startsWith("/providers")
681
+ ? "providers"
682
+ : location.startsWith("/watchdog")
683
+ ? "watchdog"
684
+ : location.startsWith("/envars")
685
+ ? "envars"
686
+ : location.startsWith("/webhooks")
687
+ ? "webhooks"
688
+ : "general";
689
+
690
+ useEffect(() => {
691
+ setSidebarTab((currentTab) => {
692
+ if (location.startsWith("/browse")) return "browse";
693
+ if (currentTab === "browse") return "menu";
694
+ return currentTab;
695
+ });
696
+ }, [location]);
697
+
698
+ useEffect(() => {
699
+ if (!isBrowseRoute) return;
700
+ if (!selectedBrowsePath) return;
701
+ setLastBrowsePath((currentPath) =>
702
+ currentPath === selectedBrowsePath ? currentPath : selectedBrowsePath,
703
+ );
704
+ }, [isBrowseRoute, selectedBrowsePath]);
705
+
706
+ useEffect(() => {
707
+ const settings = readUiSettings();
708
+ settings.sidebarWidthPx = sidebarWidthPx;
709
+ settings[kBrowseLastPathUiSettingKey] = lastBrowsePath;
710
+ try {
711
+ window.localStorage.setItem(kUiSettingsStorageKey, JSON.stringify(settings));
712
+ } catch {}
713
+ }, [sidebarWidthPx, lastBrowsePath]);
714
+
715
+ const resizeSidebarWithClientX = useCallback((clientX) => {
716
+ const shellElement = appShellRef.current;
717
+ if (!shellElement) return;
718
+ const shellBounds = shellElement.getBoundingClientRect();
719
+ const nextWidth = clampSidebarWidth(Math.round(clientX - shellBounds.left));
720
+ setSidebarWidthPx(nextWidth);
721
+ }, []);
722
+
723
+ const onSidebarResizerPointerDown = (event) => {
724
+ event.preventDefault();
725
+ setIsResizingSidebar(true);
726
+ resizeSidebarWithClientX(event.clientX);
727
+ };
728
+
729
+ useEffect(() => {
730
+ if (!isResizingSidebar) return () => {};
731
+ const onPointerMove = (event) => resizeSidebarWithClientX(event.clientX);
732
+ const onPointerUp = () => setIsResizingSidebar(false);
733
+ window.addEventListener("pointermove", onPointerMove);
734
+ window.addEventListener("pointerup", onPointerUp);
735
+ const previousUserSelect = document.body.style.userSelect;
736
+ const previousCursor = document.body.style.cursor;
737
+ document.body.style.userSelect = "none";
738
+ document.body.style.cursor = "col-resize";
739
+ return () => {
740
+ window.removeEventListener("pointermove", onPointerMove);
741
+ window.removeEventListener("pointerup", onPointerUp);
742
+ document.body.style.userSelect = previousUserSelect;
743
+ document.body.style.cursor = previousCursor;
744
+ };
745
+ }, [isResizingSidebar, resizeSidebarWithClientX]);
616
746
 
617
747
  const renderWebhooks = (hookName = "") => html`
618
748
  <div class="pt-4">
@@ -626,91 +756,53 @@ const App = () => {
626
756
  `;
627
757
 
628
758
  return html`
629
- <div class="app-shell">
759
+ <div
760
+ class="app-shell"
761
+ ref=${appShellRef}
762
+ style=${{ gridTemplateColumns: `${sidebarWidthPx}px 0px minmax(0, 1fr)` }}
763
+ >
630
764
  <${GlobalRestartBanner}
631
765
  visible=${restartRequired}
632
766
  restarting=${restartingGateway}
633
767
  onRestart=${handleGatewayRestart}
634
768
  />
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>
769
+ <${AppSidebar}
770
+ mobileSidebarOpen=${mobileSidebarOpen}
771
+ authEnabled=${authEnabled}
772
+ menuRef=${menuRef}
773
+ menuOpen=${menuOpen}
774
+ onToggleMenu=${() => setMenuOpen((open) => !open)}
775
+ onLogout=${handleSidebarLogout}
776
+ sidebarTab=${sidebarTab}
777
+ onSelectSidebarTab=${handleSelectSidebarTab}
778
+ navSections=${kNavSections}
779
+ selectedNavId=${selectedNavId}
780
+ onSelectNavItem=${handleSelectNavItem}
781
+ selectedBrowsePath=${selectedBrowsePath}
782
+ onSelectBrowseFile=${navigateToBrowseFile}
783
+ acHasUpdate=${acHasUpdate}
784
+ acLatest=${acLatest}
785
+ acDismissed=${acDismissed}
786
+ acUpdating=${acUpdating}
787
+ onAcUpdate=${handleAcUpdate}
788
+ />
789
+ <div
790
+ class=${`sidebar-resizer ${isResizingSidebar ? "is-resizing" : ""}`}
791
+ onpointerdown=${onSidebarResizerPointerDown}
792
+ role="separator"
793
+ aria-orientation="vertical"
794
+ aria-label="Resize sidebar"
795
+ ></div>
707
796
 
708
797
  <div
709
798
  class=${`mobile-sidebar-overlay ${mobileSidebarOpen ? "active" : ""}`}
710
799
  onclick=${() => setMobileSidebarOpen(false)}
711
800
  />
712
801
 
713
- <div class="app-content" onscroll=${handleAppContentScroll}>
802
+ <div
803
+ class=${`app-content ${isBrowseRoute ? "browse-mode" : ""}`}
804
+ onscroll=${handleAppContentScroll}
805
+ >
714
806
  <div class=${`mobile-topbar ${mobileTopbarScrolled ? "is-scrolled" : ""}`}>
715
807
  <button
716
808
  class="mobile-topbar-menu"
@@ -728,61 +820,70 @@ const App = () => {
728
820
  <span style="color: var(--accent)">alpha</span>claw
729
821
  </span>
730
822
  </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}
823
+ <div class=${isBrowseRoute ? "w-full" : "max-w-2xl w-full mx-auto"}>
824
+ ${isBrowseRoute
825
+ ? html`
826
+ <${FileViewer}
827
+ filePath=${selectedBrowsePath}
768
828
  />
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}>
829
+ `
830
+ : html`
831
+ <${Switch}>
832
+ <${Route} path="/telegram">
833
+ <div class="pt-4">
834
+ <${TelegramWorkspace} onBack=${exitSubScreen} />
835
+ </div>
836
+ </${Route}>
837
+ <${Route} path="/general">
838
+ <div class="pt-4">
839
+ <${GeneralTab}
840
+ statusData=${sharedStatus}
841
+ watchdogData=${sharedWatchdogStatus}
842
+ onRefreshStatuses=${refreshSharedStatuses}
843
+ onSwitchTab=${(nextTab) => setLocation(`/${nextTab}`)}
844
+ onNavigate=${navigateToSubScreen}
845
+ isActive=${location === "/general"}
846
+ restartingGateway=${restartingGateway}
847
+ onRestartGateway=${handleGatewayRestart}
848
+ restartSignal=${gatewayRestartSignal}
849
+ />
850
+ </div>
851
+ </${Route}>
852
+ <${Route} path="/providers">
853
+ <div class="pt-4">
854
+ <${Providers} onRestartRequired=${setRestartRequired} />
855
+ </div>
856
+ </${Route}>
857
+ <${Route} path="/watchdog">
858
+ <div class="pt-4">
859
+ <${WatchdogTab}
860
+ gatewayStatus=${sharedStatus?.gateway || null}
861
+ openclawVersion=${sharedStatus?.openclawVersion || null}
862
+ watchdogStatus=${sharedWatchdogStatus}
863
+ onRefreshStatuses=${refreshSharedStatuses}
864
+ restartingGateway=${restartingGateway}
865
+ onRestartGateway=${handleGatewayRestart}
866
+ restartSignal=${gatewayRestartSignal}
867
+ />
868
+ </div>
869
+ </${Route}>
870
+ <${Route} path="/envars">
871
+ <div class="pt-4">
872
+ <${Envars} onRestartRequired=${setRestartRequired} />
873
+ </div>
874
+ </${Route}>
875
+ <${Route} path="/webhooks/:hookName">
876
+ ${(params) =>
877
+ renderWebhooks(decodeURIComponent(params.hookName || ""))}
878
+ </${Route}>
879
+ <${Route} path="/webhooks">
880
+ ${() => renderWebhooks("")}
881
+ </${Route}>
882
+ <${Route}>
883
+ <${RouteRedirect} to="/general" />
884
+ </${Route}>
885
+ </${Switch}>
886
+ `}
786
887
  </div>
787
888
  <${ToastContainer}
788
889
  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