@chrysb/alphaclaw 0.3.5-beta.1 → 0.4.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 (37) hide show
  1. package/bin/alphaclaw.js +1 -31
  2. package/lib/public/assets/icons/google_icon.svg +8 -0
  3. package/lib/public/css/explorer.css +53 -0
  4. package/lib/public/js/app.js +126 -105
  5. package/lib/public/js/components/credentials-modal.js +36 -8
  6. package/lib/public/js/components/file-tree.js +212 -22
  7. package/lib/public/js/components/file-viewer/index.js +44 -6
  8. package/lib/public/js/components/file-viewer/status-banners.js +11 -6
  9. package/lib/public/js/components/file-viewer/toolbar.js +43 -1
  10. package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +6 -0
  11. package/lib/public/js/components/file-viewer/use-file-diff.js +11 -0
  12. package/lib/public/js/components/file-viewer/use-file-loader.js +12 -2
  13. package/lib/public/js/components/file-viewer/use-file-viewer.js +94 -2
  14. package/lib/public/js/components/google/account-row.js +98 -0
  15. package/lib/public/js/components/google/add-account-modal.js +93 -0
  16. package/lib/public/js/components/google/index.js +439 -0
  17. package/lib/public/js/components/google/use-google-accounts.js +41 -0
  18. package/lib/public/js/components/icons.js +26 -0
  19. package/lib/public/js/components/sidebar-git-panel.js +43 -14
  20. package/lib/public/js/components/sidebar.js +91 -75
  21. package/lib/public/js/lib/api.js +72 -8
  22. package/lib/public/js/lib/browse-file-policies.js +29 -11
  23. package/lib/public/js/lib/syntax-highlighters/index.js +6 -5
  24. package/lib/public/shared/browse-file-policies.json +13 -0
  25. package/lib/server/constants.js +19 -7
  26. package/lib/server/google-state.js +187 -0
  27. package/lib/server/helpers.js +12 -4
  28. package/lib/server/onboarding/github.js +21 -2
  29. package/lib/server/onboarding/index.js +1 -3
  30. package/lib/server/onboarding/openclaw.js +3 -0
  31. package/lib/server/onboarding/workspace.js +40 -0
  32. package/lib/server/routes/browse/index.js +90 -2
  33. package/lib/server/routes/google.js +414 -213
  34. package/lib/setup/gitignore +3 -0
  35. package/lib/setup/hourly-git-sync.sh +28 -1
  36. package/package.json +1 -1
  37. package/lib/public/js/components/google.js +0 -228
package/bin/alphaclaw.js CHANGED
@@ -626,37 +626,7 @@ try {
626
626
  } catch {}
627
627
 
628
628
  // ---------------------------------------------------------------------------
629
- // 10. Configure gog credentials (if env vars present)
630
- // ---------------------------------------------------------------------------
631
-
632
- if (process.env.GOG_CLIENT_CREDENTIALS_JSON && process.env.GOG_REFRESH_TOKEN) {
633
- try {
634
- const tmpCreds = `/tmp/gog-creds-${process.pid}.json`;
635
- const tmpToken = `/tmp/gog-token-${process.pid}.json`;
636
- fs.writeFileSync(tmpCreds, process.env.GOG_CLIENT_CREDENTIALS_JSON);
637
- execSync(`gog auth credentials set "${tmpCreds}"`, { stdio: "ignore" });
638
- fs.unlinkSync(tmpCreds);
639
- fs.writeFileSync(
640
- tmpToken,
641
- JSON.stringify({
642
- email: process.env.GOG_ACCOUNT || "",
643
- refresh_token: process.env.GOG_REFRESH_TOKEN,
644
- }),
645
- );
646
- execSync(`gog auth tokens import "${tmpToken}"`, { stdio: "ignore" });
647
- fs.unlinkSync(tmpToken);
648
- console.log(
649
- `[alphaclaw] gog CLI configured for ${process.env.GOG_ACCOUNT || "account"}`,
650
- );
651
- } catch (e) {
652
- console.log(`[alphaclaw] gog credentials setup skipped: ${e.message}`);
653
- }
654
- } else {
655
- console.log("[alphaclaw] Google credentials not set -- skipping gog setup");
656
- }
657
-
658
- // ---------------------------------------------------------------------------
659
- // 11. Reconcile channels if already onboarded
629
+ // 10. Reconcile channels if already onboarded
660
630
  // ---------------------------------------------------------------------------
661
631
 
662
632
  const configPath = path.join(openclawDir, "openclaw.json");
@@ -0,0 +1,8 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-labelledby="title">
2
+ <title>Google</title>
3
+ <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
4
+ <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" fill="#34A853"/>
5
+ <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" fill="#FBBC05"/>
6
+ <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
7
+ <path d="M1 1h22v22H1z" fill="none"/>
8
+ </svg>
@@ -216,6 +216,28 @@
216
216
  background: var(--bg-hover);
217
217
  }
218
218
 
219
+ .tree-folder.active {
220
+ background: var(--bg-active);
221
+ color: var(--accent);
222
+ }
223
+
224
+ .tree-folder.active .arrow {
225
+ color: var(--accent);
226
+ }
227
+
228
+ .tree-folder-toggle {
229
+ border: 0;
230
+ margin: 0;
231
+ padding: 0;
232
+ background: transparent;
233
+ color: inherit;
234
+ display: inline-flex;
235
+ align-items: center;
236
+ justify-content: center;
237
+ cursor: pointer;
238
+ flex: 0 0 auto;
239
+ }
240
+
219
241
  .arrow {
220
242
  font-size: 10px;
221
243
  transition: transform 0.15s;
@@ -565,6 +587,37 @@
565
587
  flex-shrink: 0;
566
588
  }
567
589
 
590
+ .file-viewer-icon-action {
591
+ display: inline-flex;
592
+ align-items: center;
593
+ justify-content: center;
594
+ width: 28px;
595
+ height: 28px;
596
+ margin-right: 10px;
597
+ border: 1px solid var(--border);
598
+ border-radius: 8px;
599
+ background: rgba(255, 255, 255, 0.02);
600
+ color: var(--text-muted);
601
+ transition: border-color 0.12s ease, color 0.12s ease, background 0.12s ease;
602
+ }
603
+
604
+ .file-viewer-icon-action:hover {
605
+ border-color: rgba(148, 163, 184, 0.45);
606
+ color: var(--text);
607
+ background: rgba(148, 163, 184, 0.08);
608
+ }
609
+
610
+ .file-viewer-icon-action.is-disabled {
611
+ opacity: 0.45;
612
+ cursor: not-allowed;
613
+ }
614
+
615
+ .file-viewer-icon-action-icon {
616
+ width: 14px;
617
+ height: 14px;
618
+ flex-shrink: 0;
619
+ }
620
+
568
621
  .file-viewer-view-toggle {
569
622
  display: flex;
570
623
  align-items: center;
@@ -28,7 +28,7 @@ import { Gateway } from "./components/gateway.js";
28
28
  import { Channels, ALL_CHANNELS } from "./components/channels.js";
29
29
  import { Pairings } from "./components/pairings.js";
30
30
  import { DevicePairings } from "./components/device-pairings.js";
31
- import { Google } from "./components/google.js";
31
+ import { Google } from "./components/google/index.js";
32
32
  import { Features } from "./components/features.js";
33
33
  import { Providers } from "./components/providers.js";
34
34
  import { Welcome } from "./components/welcome.js";
@@ -52,6 +52,7 @@ const kSidebarMinWidthPx = 180;
52
52
  const kSidebarMaxWidthPx = 460;
53
53
  const kBrowseLastPathUiSettingKey = "browseLastPath";
54
54
  const kLastMenuRouteUiSettingKey = "lastMenuRoute";
55
+ const normalizeBrowsePath = (value) => String(value || "").replace(/^\/+|\/+$/g, "");
55
56
 
56
57
  const clampSidebarWidth = (value) =>
57
58
  Math.max(kSidebarMinWidthPx, Math.min(kSidebarMaxWidthPx, value));
@@ -106,7 +107,6 @@ const GeneralTab = ({
106
107
  onOpenclawVersionActionComplete = () => {},
107
108
  onOpenclawUpdate,
108
109
  }) => {
109
- const [googleKey, setGoogleKey] = useState(0);
110
110
  const [dashboardLoading, setDashboardLoading] = useState(false);
111
111
  const [repairingWatchdog, setRepairingWatchdog] = useState(false);
112
112
  const status = statusData;
@@ -179,7 +179,6 @@ const GeneralTab = ({
179
179
  onRefreshStatuses();
180
180
  pairingsPoll.refresh();
181
181
  devicePoll.refresh();
182
- setGoogleKey((k) => k + 1);
183
182
  }, [isActive]);
184
183
 
185
184
  useEffect(() => {
@@ -280,7 +279,7 @@ const GeneralTab = ({
280
279
  onReject=${handleReject}
281
280
  />
282
281
  <${Features} onSwitchTab=${onSwitchTab} />
283
- <${Google} key=${googleKey} gatewayStatus=${gatewayStatus} />
282
+ <${Google} gatewayStatus=${gatewayStatus} />
284
283
 
285
284
  ${repo &&
286
285
  html`
@@ -646,9 +645,27 @@ const App = () => {
646
645
  setLocation(`/${screen}`);
647
646
  setMobileSidebarOpen(false);
648
647
  };
648
+ const handleBrowsePreviewFile = useCallback((nextPreviewPath) => {
649
+ const normalizedPreviewPath = normalizeBrowsePath(nextPreviewPath);
650
+ setBrowsePreviewPath(normalizedPreviewPath);
651
+ }, []);
649
652
  const navigateToBrowseFile = (relativePath, options = {}) => {
650
- setBrowsePreviewPath("");
651
- setLocation(buildBrowseRoute(relativePath, options));
653
+ const normalizedTargetPath = normalizeBrowsePath(relativePath);
654
+ const selectingDirectory =
655
+ !!options.directory || String(relativePath || "").trim().endsWith("/");
656
+ const shouldPreservePreview = selectingDirectory && !!options.preservePreview;
657
+ const activePath = normalizeBrowsePath(
658
+ browsePreviewPath || selectedBrowsePath || "",
659
+ );
660
+ const nextPreviewPath =
661
+ shouldPreservePreview && activePath && activePath !== normalizedTargetPath
662
+ ? activePath
663
+ : "";
664
+ setBrowsePreviewPath(nextPreviewPath);
665
+ const routeOptions = selectingDirectory
666
+ ? { ...options, view: "edit" }
667
+ : options;
668
+ setLocation(buildBrowseRoute(normalizedTargetPath, routeOptions));
652
669
  setMobileSidebarOpen(false);
653
670
  };
654
671
  const handleSidebarLogout = async () => {
@@ -879,7 +896,7 @@ const App = () => {
879
896
  onSelectNavItem=${handleSelectNavItem}
880
897
  selectedBrowsePath=${selectedBrowsePath}
881
898
  onSelectBrowseFile=${navigateToBrowseFile}
882
- onPreviewBrowseFile=${setBrowsePreviewPath}
899
+ onPreviewBrowseFile=${handleBrowsePreviewFile}
883
900
  acHasUpdate=${acHasUpdate}
884
901
  acLatest=${acLatest}
885
902
  acDismissed=${acDismissed}
@@ -921,112 +938,116 @@ const App = () => {
921
938
  </span>
922
939
  </div>
923
940
  <div class=${isBrowseRoute ? "w-full" : "max-w-2xl w-full mx-auto"}>
924
- ${isBrowseRoute
925
- ? html`
926
- <${FileViewer}
927
- filePath=${activeBrowsePath}
928
- isPreviewOnly=${Boolean(
929
- browsePreviewPath &&
930
- browsePreviewPath !== selectedBrowsePath,
931
- )}
932
- browseView=${browseViewerMode}
933
- onRequestEdit=${(targetPath) => {
934
- const normalizedTargetPath = String(targetPath || "");
935
- if (
936
- normalizedTargetPath &&
937
- normalizedTargetPath !== selectedBrowsePath
938
- ) {
939
- navigateToBrowseFile(normalizedTargetPath, { view: "edit" });
940
- return;
941
- }
942
- setLocation(buildBrowseRoute(selectedBrowsePath, { view: "edit" }));
943
- }}
941
+ <div style=${{ display: isBrowseRoute ? "block" : "none" }}>
942
+ <${FileViewer}
943
+ filePath=${activeBrowsePath}
944
+ isPreviewOnly=${false}
945
+ browseView=${browseViewerMode}
946
+ onRequestEdit=${(targetPath) => {
947
+ const normalizedTargetPath = String(targetPath || "");
948
+ if (
949
+ normalizedTargetPath &&
950
+ normalizedTargetPath !== selectedBrowsePath
951
+ ) {
952
+ navigateToBrowseFile(normalizedTargetPath, { view: "edit" });
953
+ return;
954
+ }
955
+ setLocation(buildBrowseRoute(selectedBrowsePath, { view: "edit" }));
956
+ }}
957
+ onRequestClearSelection=${() => {
958
+ setBrowsePreviewPath("");
959
+ setLocation("/browse");
960
+ }}
961
+ />
962
+ </div>
963
+ <div style=${{ display: isBrowseRoute ? "none" : "block" }}>
964
+ <div style=${{ display: location === "/general" ? "block" : "none" }}>
965
+ <div class="pt-4">
966
+ <${GeneralTab}
967
+ statusData=${sharedStatus}
968
+ watchdogData=${sharedWatchdogStatus}
969
+ onRefreshStatuses=${refreshSharedStatuses}
970
+ onSwitchTab=${(nextTab) => setLocation(`/${nextTab}`)}
971
+ onNavigate=${navigateToSubScreen}
972
+ isActive=${location === "/general"}
973
+ restartingGateway=${restartingGateway}
974
+ onRestartGateway=${handleGatewayRestart}
975
+ restartSignal=${gatewayRestartSignal}
976
+ openclawUpdateInProgress=${openclawUpdateInProgress}
977
+ onOpenclawVersionActionComplete=${handleOpenclawVersionActionComplete}
978
+ onOpenclawUpdate=${handleOpenclawUpdate}
944
979
  />
945
- `
946
- : html`
947
- <${Switch}>
948
- <${Route} path="/telegram">
949
- <div class="pt-4">
950
- <${TelegramWorkspace} onBack=${exitSubScreen} />
951
- </div>
952
- </${Route}>
953
- <${Route} path="/general">
954
- <div class="pt-4">
955
- <${GeneralTab}
956
- statusData=${sharedStatus}
957
- watchdogData=${sharedWatchdogStatus}
958
- onRefreshStatuses=${refreshSharedStatuses}
959
- onSwitchTab=${(nextTab) => setLocation(`/${nextTab}`)}
960
- onNavigate=${navigateToSubScreen}
961
- isActive=${location === "/general"}
962
- restartingGateway=${restartingGateway}
963
- onRestartGateway=${handleGatewayRestart}
964
- restartSignal=${gatewayRestartSignal}
965
- openclawUpdateInProgress=${openclawUpdateInProgress}
966
- onOpenclawVersionActionComplete=${handleOpenclawVersionActionComplete}
967
- onOpenclawUpdate=${handleOpenclawUpdate}
968
- />
969
- </div>
970
- </${Route}>
971
- <${Route} path="/providers">
972
- <div class="pt-4">
973
- <${Providers} onRestartRequired=${setRestartRequired} />
974
- </div>
975
- </${Route}>
976
- <${Route} path="/watchdog">
977
- <div class="pt-4">
978
- <${WatchdogTab}
979
- gatewayStatus=${sharedStatus?.gateway || null}
980
- openclawVersion=${sharedStatus?.openclawVersion || null}
981
- watchdogStatus=${sharedWatchdogStatus}
982
- onRefreshStatuses=${refreshSharedStatuses}
983
- restartingGateway=${restartingGateway}
984
- onRestartGateway=${handleGatewayRestart}
985
- restartSignal=${gatewayRestartSignal}
986
- openclawUpdateInProgress=${openclawUpdateInProgress}
987
- onOpenclawVersionActionComplete=${handleOpenclawVersionActionComplete}
988
- onOpenclawUpdate=${handleOpenclawUpdate}
989
- />
990
- </div>
991
- </${Route}>
992
- <${Route} path="/usage/:sessionId">
993
- ${(params) => html`
980
+ </div>
981
+ </div>
982
+ ${!isBrowseRoute && location !== "/general"
983
+ ? html`
984
+ <${Switch}>
985
+ <${Route} path="/telegram">
986
+ <div class="pt-4">
987
+ <${TelegramWorkspace} onBack=${exitSubScreen} />
988
+ </div>
989
+ </${Route}>
990
+ <${Route} path="/providers">
991
+ <div class="pt-4">
992
+ <${Providers} onRestartRequired=${setRestartRequired} />
993
+ </div>
994
+ </${Route}>
995
+ <${Route} path="/watchdog">
996
+ <div class="pt-4">
997
+ <${WatchdogTab}
998
+ gatewayStatus=${sharedStatus?.gateway || null}
999
+ openclawVersion=${sharedStatus?.openclawVersion || null}
1000
+ watchdogStatus=${sharedWatchdogStatus}
1001
+ onRefreshStatuses=${refreshSharedStatuses}
1002
+ restartingGateway=${restartingGateway}
1003
+ onRestartGateway=${handleGatewayRestart}
1004
+ restartSignal=${gatewayRestartSignal}
1005
+ openclawUpdateInProgress=${openclawUpdateInProgress}
1006
+ onOpenclawVersionActionComplete=${handleOpenclawVersionActionComplete}
1007
+ onOpenclawUpdate=${handleOpenclawUpdate}
1008
+ />
1009
+ </div>
1010
+ </${Route}>
1011
+ <${Route} path="/usage/:sessionId">
1012
+ ${(params) => html`
1013
+ <div class="pt-4">
1014
+ <${UsageTab}
1015
+ sessionId=${decodeURIComponent(params.sessionId || "")}
1016
+ onSelectSession=${(id) =>
1017
+ setLocation(`/usage/${encodeURIComponent(String(id || ""))}`)}
1018
+ onBackToSessions=${() => setLocation("/usage")}
1019
+ />
1020
+ </div>
1021
+ `}
1022
+ </${Route}>
1023
+ <${Route} path="/usage">
994
1024
  <div class="pt-4">
995
1025
  <${UsageTab}
996
- sessionId=${decodeURIComponent(params.sessionId || "")}
997
1026
  onSelectSession=${(id) =>
998
1027
  setLocation(`/usage/${encodeURIComponent(String(id || ""))}`)}
999
1028
  onBackToSessions=${() => setLocation("/usage")}
1000
1029
  />
1001
1030
  </div>
1002
- `}
1003
- </${Route}>
1004
- <${Route} path="/usage">
1005
- <div class="pt-4">
1006
- <${UsageTab}
1007
- onSelectSession=${(id) =>
1008
- setLocation(`/usage/${encodeURIComponent(String(id || ""))}`)}
1009
- onBackToSessions=${() => setLocation("/usage")}
1010
- />
1011
- </div>
1012
- </${Route}>
1013
- <${Route} path="/envars">
1014
- <div class="pt-4">
1015
- <${Envars} onRestartRequired=${setRestartRequired} />
1016
- </div>
1017
- </${Route}>
1018
- <${Route} path="/webhooks/:hookName">
1019
- ${(params) =>
1020
- renderWebhooks(decodeURIComponent(params.hookName || ""))}
1021
- </${Route}>
1022
- <${Route} path="/webhooks">
1023
- ${() => renderWebhooks("")}
1024
- </${Route}>
1025
- <${Route}>
1026
- <${RouteRedirect} to="/general" />
1027
- </${Route}>
1028
- </${Switch}>
1029
- `}
1031
+ </${Route}>
1032
+ <${Route} path="/envars">
1033
+ <div class="pt-4">
1034
+ <${Envars} onRestartRequired=${setRestartRequired} />
1035
+ </div>
1036
+ </${Route}>
1037
+ <${Route} path="/webhooks/:hookName">
1038
+ ${(params) =>
1039
+ renderWebhooks(decodeURIComponent(params.hookName || ""))}
1040
+ </${Route}>
1041
+ <${Route} path="/webhooks">
1042
+ ${() => renderWebhooks("")}
1043
+ </${Route}>
1044
+ <${Route}>
1045
+ <${RouteRedirect} to="/general" />
1046
+ </${Route}>
1047
+ </${Switch}>
1048
+ `
1049
+ : null}
1050
+ </div>
1030
1051
  </div>
1031
1052
  <${ToastContainer}
1032
1053
  className="fixed bottom-10 right-4 z-50 space-y-2 pointer-events-none"
@@ -1,5 +1,5 @@
1
1
  import { h } from "https://esm.sh/preact";
2
- import { useState, useRef } from "https://esm.sh/preact/hooks";
2
+ import { useEffect, useRef, useState } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
4
  import { saveGoogleCredentials } from "../lib/api.js";
5
5
  import { SecretInput } from "./secret-input.js";
@@ -9,16 +9,37 @@ import { ActionButton } from "./action-button.js";
9
9
  import { CloseIcon } from "./icons.js";
10
10
  const html = htm.bind(h);
11
11
 
12
- export const CredentialsModal = ({ visible, onClose, onSaved }) => {
12
+ export const CredentialsModal = ({
13
+ visible,
14
+ onClose,
15
+ onSaved,
16
+ title = "Connect Google Workspace",
17
+ submitLabel = "Connect Google",
18
+ defaultInstrType = "workspace",
19
+ client = "default",
20
+ personal = false,
21
+ accountId = "",
22
+ initialValues = {},
23
+ }) => {
13
24
  const [clientId, setClientId] = useState("");
14
25
  const [clientSecret, setClientSecret] = useState("");
15
26
  const [email, setEmail] = useState("");
16
27
  const [error, setError] = useState("");
17
28
  const [saving, setSaving] = useState(false);
18
- const [instrType, setInstrType] = useState("workspace");
29
+ const [instrType, setInstrType] = useState(defaultInstrType);
19
30
  const [redirectUriCopied, setRedirectUriCopied] = useState(false);
20
31
  const fileRef = useRef(null);
21
32
 
33
+ useEffect(() => {
34
+ if (!visible) return;
35
+ setClientId(String(initialValues.clientId || ""));
36
+ setClientSecret(String(initialValues.clientSecret || ""));
37
+ setEmail(String(initialValues.email || ""));
38
+ setInstrType(defaultInstrType);
39
+ setError("");
40
+ setRedirectUriCopied(false);
41
+ }, [visible, initialValues, defaultInstrType]);
42
+
22
43
  if (!visible) return null;
23
44
 
24
45
  const redirectUri = `${window.location.origin}/auth/google/callback`;
@@ -50,15 +71,22 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
50
71
  const submit = async () => {
51
72
  setError("");
52
73
  if (!clientId || !clientSecret || !email) {
53
- setError("All fields required");
74
+ setError("Client ID, Client Secret, and Email are required");
54
75
  return;
55
76
  }
56
77
  setSaving(true);
57
78
  try {
58
- const data = await saveGoogleCredentials(clientId, clientSecret, email);
79
+ const data = await saveGoogleCredentials({
80
+ clientId,
81
+ clientSecret,
82
+ email,
83
+ client,
84
+ personal,
85
+ accountId,
86
+ });
59
87
  if (data.ok) {
60
88
  onClose();
61
- onSaved();
89
+ onSaved?.(data.account);
62
90
  } else setError(data.error || "Failed to save credentials");
63
91
  } catch {
64
92
  setError("Request failed");
@@ -104,7 +132,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
104
132
  panelClassName="bg-modal border border-border rounded-xl p-6 max-w-lg w-full space-y-4"
105
133
  >
106
134
  <${PageHeader}
107
- title="Connect Google Workspace"
135
+ title=${title}
108
136
  actions=${html`
109
137
  <button
110
138
  type="button"
@@ -338,7 +366,7 @@ export const CredentialsModal = ({ visible, onClose, onSaved }) => {
338
366
  loading=${saving}
339
367
  tone="primary"
340
368
  size="lg"
341
- idleLabel="Connect Google"
369
+ idleLabel=${submitLabel}
342
370
  loadingLabel="Saving..."
343
371
  className="w-full px-4 py-2 rounded-lg text-sm"
344
372
  />