@chrysb/alphaclaw 0.3.5-beta.1 → 0.4.1-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 (68) 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/css/shell.css +21 -19
  5. package/lib/public/css/theme.css +17 -0
  6. package/lib/public/js/app.js +205 -109
  7. package/lib/public/js/components/credentials-modal.js +36 -8
  8. package/lib/public/js/components/file-tree.js +212 -22
  9. package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
  10. package/lib/public/js/components/file-viewer/index.js +47 -6
  11. package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
  12. package/lib/public/js/components/file-viewer/status-banners.js +11 -6
  13. package/lib/public/js/components/file-viewer/toolbar.js +56 -1
  14. package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +6 -0
  15. package/lib/public/js/components/file-viewer/use-file-diff.js +11 -0
  16. package/lib/public/js/components/file-viewer/use-file-loader.js +12 -2
  17. package/lib/public/js/components/file-viewer/use-file-viewer.js +142 -15
  18. package/lib/public/js/components/google/account-row.js +131 -0
  19. package/lib/public/js/components/google/add-account-modal.js +93 -0
  20. package/lib/public/js/components/google/gmail-setup-wizard.js +450 -0
  21. package/lib/public/js/components/google/gmail-watch-toggle.js +81 -0
  22. package/lib/public/js/components/google/index.js +553 -0
  23. package/lib/public/js/components/google/use-gmail-watch.js +140 -0
  24. package/lib/public/js/components/google/use-google-accounts.js +41 -0
  25. package/lib/public/js/components/icons.js +26 -0
  26. package/lib/public/js/components/scope-picker.js +1 -1
  27. package/lib/public/js/components/sidebar-git-panel.js +48 -20
  28. package/lib/public/js/components/sidebar.js +93 -75
  29. package/lib/public/js/components/toast.js +11 -7
  30. package/lib/public/js/components/usage-tab/constants.js +31 -0
  31. package/lib/public/js/components/usage-tab/formatters.js +24 -0
  32. package/lib/public/js/components/usage-tab/index.js +72 -0
  33. package/lib/public/js/components/usage-tab/overview-section.js +147 -0
  34. package/lib/public/js/components/usage-tab/sessions-section.js +175 -0
  35. package/lib/public/js/components/usage-tab/use-usage-tab.js +241 -0
  36. package/lib/public/js/components/webhooks.js +182 -129
  37. package/lib/public/js/lib/api.js +178 -9
  38. package/lib/public/js/lib/browse-file-policies.js +29 -11
  39. package/lib/public/js/lib/format.js +71 -0
  40. package/lib/public/js/lib/syntax-highlighters/index.js +6 -5
  41. package/lib/public/shared/browse-file-policies.json +13 -0
  42. package/lib/server/constants.js +47 -7
  43. package/lib/server/gmail-push.js +109 -0
  44. package/lib/server/gmail-serve.js +254 -0
  45. package/lib/server/gmail-watch.js +725 -0
  46. package/lib/server/google-state.js +317 -0
  47. package/lib/server/helpers.js +17 -11
  48. package/lib/server/internal-files-migration.js +31 -3
  49. package/lib/server/onboarding/github.js +21 -2
  50. package/lib/server/onboarding/index.js +1 -3
  51. package/lib/server/onboarding/openclaw.js +3 -0
  52. package/lib/server/onboarding/workspace.js +40 -0
  53. package/lib/server/routes/browse/index.js +90 -2
  54. package/lib/server/routes/gmail.js +128 -0
  55. package/lib/server/routes/google.js +433 -213
  56. package/lib/server/routes/system.js +107 -0
  57. package/lib/server/routes/usage.js +29 -2
  58. package/lib/server/routes/webhooks.js +52 -17
  59. package/lib/server/usage-db.js +283 -15
  60. package/lib/server/watchdog.js +66 -0
  61. package/lib/server/webhook-middleware.js +99 -1
  62. package/lib/server/webhooks.js +214 -65
  63. package/lib/server.js +27 -0
  64. package/lib/setup/gitignore +6 -0
  65. package/lib/setup/hourly-git-sync.sh +29 -2
  66. package/package.json +1 -1
  67. package/lib/public/js/components/google.js +0 -228
  68. package/lib/public/js/components/usage-tab.js +0 -531
@@ -9,11 +9,16 @@ import {
9
9
  fetchWebhookRequests,
10
10
  fetchWebhooks,
11
11
  } from "../lib/api.js";
12
+ import {
13
+ formatLocaleDateTime,
14
+ formatLocaleDateTimeWithTodayTime,
15
+ } from "../lib/format.js";
12
16
  import { showToast } from "./toast.js";
13
17
  import { PageHeader } from "./page-header.js";
14
18
  import { ConfirmDialog } from "./confirm-dialog.js";
15
19
  import { ActionButton } from "./action-button.js";
16
20
  import { ModalShell } from "./modal-shell.js";
21
+ import { Badge } from "./badge.js";
17
22
  import { CloseIcon } from "./icons.js";
18
23
 
19
24
  const html = htm.bind(h);
@@ -21,29 +26,11 @@ const kNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
21
26
  const kStatusFilters = ["all", "success", "error"];
22
27
 
23
28
  const formatDateTime = (value) => {
24
- if (!value) return "—";
25
- try {
26
- return new Date(value).toLocaleString();
27
- } catch {
28
- return value;
29
- }
29
+ return formatLocaleDateTime(value, { fallback: "—" });
30
30
  };
31
31
 
32
32
  const formatLastReceived = (value) => {
33
- if (!value) return "—";
34
- try {
35
- const timestamp = new Date(value);
36
- const now = new Date();
37
- const isSameDay =
38
- timestamp.getFullYear() === now.getFullYear() &&
39
- timestamp.getMonth() === now.getMonth() &&
40
- timestamp.getDate() === now.getDate();
41
- return isSameDay
42
- ? timestamp.toLocaleTimeString()
43
- : timestamp.toLocaleString();
44
- } catch {
45
- return value;
46
- }
33
+ return formatLocaleDateTimeWithTodayTime(value, { fallback: "—" });
47
34
  };
48
35
 
49
36
  const formatBytes = (size) => {
@@ -197,6 +184,7 @@ export const Webhooks = ({
197
184
  onSelectHook = () => {},
198
185
  onBackToList = () => {},
199
186
  onRestartRequired = () => {},
187
+ onOpenFile = () => {},
200
188
  }) => {
201
189
  const [isCreating, setIsCreating] = useState(false);
202
190
  const [newName, setNewName] = useState("");
@@ -238,12 +226,13 @@ export const Webhooks = ({
238
226
  );
239
227
 
240
228
  const selectedWebhook = detailPoll.data;
229
+ const selectedWebhookManaged = Boolean(selectedWebhook?.managed);
241
230
  const requests = requestsPoll.data?.requests || [];
242
231
  const webhookUrl =
243
232
  selectedWebhook?.fullUrl || `.../hooks/${selectedHookName}`;
244
233
  const webhookUrlWithQueryToken =
245
234
  selectedWebhook?.queryStringUrl ||
246
- `${webhookUrl}${webhookUrl.includes("?") ? "&" : "?"}token=<WEBHOOK_TOKEN>`;
235
+ `${webhookUrl}${webhookUrl.includes("?") ? "&" : "?"}token=<OPENCLAW_HOOKS_TOKEN>`;
247
236
  const derivedTokenFromQuery = (() => {
248
237
  try {
249
238
  const parsed = new URL(webhookUrlWithQueryToken);
@@ -256,17 +245,33 @@ export const Webhooks = ({
256
245
  selectedWebhook?.authHeaderValue ||
257
246
  (derivedTokenFromQuery
258
247
  ? `Authorization: Bearer ${derivedTokenFromQuery}`
259
- : "Authorization: Bearer <WEBHOOK_TOKEN>");
248
+ : "Authorization: Bearer <OPENCLAW_HOOKS_TOKEN>");
260
249
  const bearerTokenValue = authHeaderValue.startsWith("Authorization: ")
261
250
  ? authHeaderValue.slice("Authorization: ".length)
262
251
  : authHeaderValue;
263
- const webhookTestPayload = useMemo(
264
- () => ({
252
+ const webhookTestPayload = useMemo(() => {
253
+ if (String(selectedHookName || "").trim().toLowerCase() === "gmail") {
254
+ return {
255
+ payload: {
256
+ account: "test@gmail.com",
257
+ messages: [
258
+ {
259
+ id: "test-message-1",
260
+ from: "alerts@example.com",
261
+ to: ["test@gmail.com"],
262
+ subject: "Test Gmail webhook event",
263
+ snippet: "This is a simulated Gmail message payload for webhook testing.",
264
+ receivedAt: new Date().toISOString(),
265
+ },
266
+ ],
267
+ },
268
+ };
269
+ }
270
+ return {
265
271
  source: "manual-test",
266
- message: `This is a test of the ${selectedHookName || "webhook"} webhook. Please acknowledge receipt.`,
267
- }),
268
- [selectedHookName],
269
- );
272
+ message: `This is a test of the ${selectedHookName || "webhook"} webhook.`,
273
+ };
274
+ }, [selectedHookName]);
270
275
  const webhookTestPayloadJson = JSON.stringify(webhookTestPayload);
271
276
  const curlCommandHeaders =
272
277
  `curl -X POST "${webhookUrl}" ` +
@@ -277,8 +282,9 @@ export const Webhooks = ({
277
282
  `curl -X POST "${webhookUrlWithQueryToken}" ` +
278
283
  `-H "Content-Type: application/json" ` +
279
284
  `-d '${webhookTestPayloadJson}'`;
285
+ const effectiveAuthMode = selectedWebhookManaged ? "headers" : authMode;
280
286
  const activeCurlCommand =
281
- authMode === "query" ? curlCommandQuery : curlCommandHeaders;
287
+ effectiveAuthMode === "query" ? curlCommandQuery : curlCommandHeaders;
282
288
 
283
289
  const canCreate = useMemo(() => {
284
290
  const name = String(newName || "")
@@ -376,9 +382,9 @@ export const Webhooks = ({
376
382
  if (!selectedHookName || sendingTestWebhook) return;
377
383
  setSendingTestWebhook(true);
378
384
  const requestUrl =
379
- authMode === "query" ? webhookUrlWithQueryToken : webhookUrl;
385
+ effectiveAuthMode === "query" ? webhookUrlWithQueryToken : webhookUrl;
380
386
  const headers = { "Content-Type": "application/json" };
381
- if (authMode === "headers") {
387
+ if (effectiveAuthMode === "headers") {
382
388
  headers.Authorization = bearerTokenValue;
383
389
  }
384
390
  try {
@@ -412,8 +418,8 @@ export const Webhooks = ({
412
418
  setSendingTestWebhook(false);
413
419
  }
414
420
  }, [
415
- authMode,
416
421
  bearerTokenValue,
422
+ effectiveAuthMode,
417
423
  requestsPoll.refresh,
418
424
  selectedHookName,
419
425
  sendingTestWebhook,
@@ -430,9 +436,9 @@ export const Webhooks = ({
430
436
  return;
431
437
  }
432
438
  const requestUrl =
433
- authMode === "query" ? webhookUrlWithQueryToken : webhookUrl;
439
+ effectiveAuthMode === "query" ? webhookUrlWithQueryToken : webhookUrl;
434
440
  const headers = { "Content-Type": "application/json" };
435
- if (authMode === "headers") {
441
+ if (effectiveAuthMode === "headers") {
436
442
  headers.Authorization = bearerTokenValue;
437
443
  }
438
444
  setReplayingRequestId(item.id);
@@ -468,8 +474,8 @@ export const Webhooks = ({
468
474
  }
469
475
  },
470
476
  [
471
- authMode,
472
477
  bearerTokenValue,
478
+ effectiveAuthMode,
473
479
  replayingRequestId,
474
480
  requestsPoll.refresh,
475
481
  webhookUrl,
@@ -482,7 +488,10 @@ export const Webhooks = ({
482
488
  await navigator.clipboard.writeText(String(value || ""));
483
489
  showToast(`${label} copied`, "success");
484
490
  } catch {
485
- showToast(`Could not copy ${String(label || "value").toLowerCase()}`, "error");
491
+ showToast(
492
+ `Could not copy ${String(label || "value").toLowerCase()}`,
493
+ "error",
494
+ );
486
495
  }
487
496
  }, []);
488
497
 
@@ -538,36 +547,40 @@ export const Webhooks = ({
538
547
  <div
539
548
  class="bg-black/20 border border-border rounded-lg p-3 space-y-4"
540
549
  >
541
- <div class="space-y-2">
542
- <p class="text-xs text-gray-500">Auth mode</p>
543
- <div class="flex items-center gap-2">
544
- <button
545
- class="text-xs px-2 py-1 rounded border transition-colors ${authMode ===
546
- "headers"
547
- ? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
548
- : "border-border text-gray-400 hover:text-gray-200"}"
549
- onclick=${() => setAuthMode("headers")}
550
- >
551
- Headers
552
- </button>
553
- <button
554
- class="text-xs px-2 py-1 rounded border transition-colors ${authMode ===
555
- "query"
556
- ? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
557
- : "border-border text-gray-400 hover:text-gray-200"}"
558
- onclick=${() => setAuthMode("query")}
559
- >
560
- Query string
561
- </button>
562
- </div>
563
- </div>
550
+ ${selectedWebhookManaged
551
+ ? null
552
+ : html`
553
+ <div class="space-y-2">
554
+ <p class="text-xs text-gray-500">Auth mode</p>
555
+ <div class="flex items-center gap-2">
556
+ <button
557
+ class="text-xs px-2 py-1 rounded border transition-colors ${authMode ===
558
+ "headers"
559
+ ? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
560
+ : "border-border text-gray-400 hover:text-gray-200"}"
561
+ onclick=${() => setAuthMode("headers")}
562
+ >
563
+ Headers
564
+ </button>
565
+ <button
566
+ class="text-xs px-2 py-1 rounded border transition-colors ${authMode ===
567
+ "query"
568
+ ? "border-cyan-400 text-cyan-200 bg-cyan-400/10"
569
+ : "border-border text-gray-400 hover:text-gray-200"}"
570
+ onclick=${() => setAuthMode("query")}
571
+ >
572
+ Query string
573
+ </button>
574
+ </div>
575
+ </div>
576
+ `}
564
577
  <div class="space-y-2">
565
578
  <p class="text-xs text-gray-500">Webhook URL</p>
566
579
  <div class="flex items-center gap-2">
567
580
  <input
568
581
  type="text"
569
582
  readonly
570
- value=${authMode === "query"
583
+ value=${effectiveAuthMode === "query"
571
584
  ? webhookUrlWithQueryToken
572
585
  : webhookUrl}
573
586
  class="h-8 flex-1 bg-black/30 border border-border rounded-lg px-3 text-xs text-gray-200 outline-none font-mono"
@@ -577,7 +590,7 @@ export const Webhooks = ({
577
590
  onclick=${async () => {
578
591
  try {
579
592
  await navigator.clipboard.writeText(
580
- authMode === "query"
593
+ effectiveAuthMode === "query"
581
594
  ? webhookUrlWithQueryToken
582
595
  : webhookUrl,
583
596
  );
@@ -591,44 +604,46 @@ export const Webhooks = ({
591
604
  </button>
592
605
  </div>
593
606
  </div>
594
- ${authMode === "headers"
595
- ? html`
596
- <div class="space-y-2">
597
- <p class="text-xs text-gray-500">Auth headers</p>
598
- <div class="flex items-center gap-2">
599
- <input
600
- type="text"
601
- readonly
602
- value=${authHeaderValue}
603
- class="h-8 flex-1 bg-black/30 border border-border rounded-lg px-3 text-xs text-gray-200 outline-none font-mono"
604
- />
605
- <button
606
- class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary shrink-0"
607
- onclick=${async () => {
608
- try {
609
- await navigator.clipboard.writeText(
610
- bearerTokenValue,
611
- );
612
- showToast("Bearer token copied", "success");
613
- } catch {
614
- showToast(
615
- "Could not copy bearer token",
616
- "error",
617
- );
618
- }
619
- }}
620
- >
621
- Copy
622
- </button>
607
+ ${selectedWebhookManaged
608
+ ? null
609
+ : effectiveAuthMode === "headers"
610
+ ? html`
611
+ <div class="space-y-2">
612
+ <p class="text-xs text-gray-500">Auth headers</p>
613
+ <div class="flex items-center gap-2">
614
+ <input
615
+ type="text"
616
+ readonly
617
+ value=${authHeaderValue}
618
+ class="h-8 flex-1 bg-black/30 border border-border rounded-lg px-3 text-xs text-gray-200 outline-none font-mono"
619
+ />
620
+ <button
621
+ class="h-8 text-xs px-2.5 rounded-lg ac-btn-secondary shrink-0"
622
+ onclick=${async () => {
623
+ try {
624
+ await navigator.clipboard.writeText(
625
+ bearerTokenValue,
626
+ );
627
+ showToast("Bearer token copied", "success");
628
+ } catch {
629
+ showToast(
630
+ "Could not copy bearer token",
631
+ "error",
632
+ );
633
+ }
634
+ }}
635
+ >
636
+ Copy
637
+ </button>
638
+ </div>
623
639
  </div>
624
- </div>
625
- `
626
- : html`
627
- <p class="text-xs text-yellow-300">
628
- Always use auth headers when possible. Query string is
629
- less secure.
630
- </p>
631
- `}
640
+ `
641
+ : html`
642
+ <p class="text-xs text-yellow-300">
643
+ Always use auth headers when possible. Query string is
644
+ less secure.
645
+ </p>
646
+ `}
632
647
  </div>
633
648
 
634
649
  <div
@@ -674,11 +689,16 @@ export const Webhooks = ({
674
689
  <div class="bg-black/20 border border-border rounded-lg p-3">
675
690
  <div class="flex items-center gap-2 text-xs text-gray-300">
676
691
  <span class="text-gray-500">Transform:</span>
677
- <code
678
- class="flex-1 min-w-0 truncate block"
679
- title=${selectedWebhook?.transformPath || ""}
680
- >${selectedWebhook?.transformPath || "—"}</code
681
- >
692
+ ${selectedWebhook?.transformPath
693
+ ? html`<button
694
+ type="button"
695
+ class="ac-tip-link flex-1 min-w-0 truncate block text-left font-mono"
696
+ title=${selectedWebhook.transformPath}
697
+ onclick=${() => onOpenFile(selectedWebhook.transformPath)}
698
+ >
699
+ ${selectedWebhook.transformPath}
700
+ </button>`
701
+ : html`<code class="flex-1 min-w-0 truncate block">—</code>`}
682
702
  <span
683
703
  class=${`ml-auto inline-flex items-center gap-1 px-1.5 py-0.5 rounded border font-sans ${
684
704
  selectedWebhook?.transformExists
@@ -700,23 +720,37 @@ export const Webhooks = ({
700
720
  <p class="text-xs text-gray-600">
701
721
  Created: ${formatDateTime(selectedWebhook?.createdAt)}
702
722
  </p>
703
- <${ActionButton}
704
- onClick=${() => {
705
- if (deleting) return;
706
- setDeleteTransformDir(true);
707
- setShowDeleteConfirm(true);
708
- }}
709
- disabled=${deleting}
710
- loading=${deleting}
711
- tone="danger"
712
- size="sm"
713
- idleLabel="Delete"
714
- loadingLabel="Deleting..."
715
- className="shrink-0 px-2.5 py-1"
716
- />
723
+ ${selectedWebhookManaged
724
+ ? null
725
+ : html`<${ActionButton}
726
+ onClick=${() => {
727
+ if (deleting) return;
728
+ setDeleteTransformDir(true);
729
+ setShowDeleteConfirm(true);
730
+ }}
731
+ disabled=${deleting}
732
+ loading=${deleting}
733
+ tone="danger"
734
+ size="sm"
735
+ idleLabel="Delete"
736
+ loadingLabel="Deleting..."
737
+ className="shrink-0 px-2.5 py-1"
738
+ />`}
717
739
  </div>
718
740
  </div>
719
741
 
742
+ ${selectedWebhookManaged
743
+ ? html`
744
+ <div
745
+ class="rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3"
746
+ >
747
+ <p class="text-xs text-yellow-200">
748
+ This webhook is managed by Gmail Watch setup and cannot
749
+ be deleted or edited from this page.
750
+ </p>
751
+ </div>
752
+ `
753
+ : null}
720
754
  <div
721
755
  class="bg-surface border border-border rounded-xl p-4 space-y-3"
722
756
  >
@@ -749,10 +783,9 @@ export const Webhooks = ({
749
783
  </p>`
750
784
  : html`
751
785
  <div class="ac-history-list">
752
- ${requests.map(
753
- (item) => {
754
- const statusTone = getRequestStatusTone(item.status);
755
- return html`
786
+ ${requests.map((item) => {
787
+ const statusTone = getRequestStatusTone(item.status);
788
+ return html`
756
789
  <details
757
790
  class="ac-history-item"
758
791
  open=${expandedRows.has(item.id)}
@@ -764,15 +797,21 @@ export const Webhooks = ({
764
797
  >
765
798
  <summary class="ac-history-summary">
766
799
  <div class="ac-history-summary-row">
767
- <span class="inline-flex items-center gap-2 min-w-0">
768
- <span class="ac-history-toggle shrink-0" aria-hidden="true"
800
+ <span
801
+ class="inline-flex items-center gap-2 min-w-0"
802
+ >
803
+ <span
804
+ class="ac-history-toggle shrink-0"
805
+ aria-hidden="true"
769
806
  >▸</span
770
807
  >
771
808
  <span class="truncate text-xs text-gray-300">
772
809
  ${formatLastReceived(item.createdAt)}
773
810
  </span>
774
811
  </span>
775
- <span class="inline-flex items-center gap-2 shrink-0">
812
+ <span
813
+ class="inline-flex items-center gap-2 shrink-0"
814
+ >
776
815
  <span class="text-xs text-gray-500"
777
816
  >${formatBytes(item.payloadSize)}</span
778
817
  >
@@ -827,7 +866,9 @@ ${jsonPretty(item.headers)}</pre
827
866
  >
828
867
  ${jsonPretty(item.payload)}</pre
829
868
  >
830
- <div class="mt-2 flex justify-start gap-2">
869
+ <div
870
+ class="mt-2 flex justify-start gap-2"
871
+ >
831
872
  <button
832
873
  class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
833
874
  onclick=${() =>
@@ -882,8 +923,7 @@ ${jsonPretty(item.gatewayBody)}</pre
882
923
  : null}
883
924
  </details>
884
925
  `;
885
- },
886
- )}
926
+ })}
887
927
  </div>
888
928
  `}
889
929
  </div>
@@ -912,11 +952,12 @@ ${jsonPretty(item.gatewayBody)}</pre
912
952
  <th class="pb-2 pr-3">Last received</th>
913
953
  <th class="pb-2 pr-3">Errors</th>
914
954
  <th class="pb-2 pr-3">Health</th>
955
+ <th class="pb-2 pr-3">Type</th>
915
956
  </tr>
916
957
  </thead>
917
958
  <tbody>
918
959
  <tr aria-hidden="true">
919
- <td class="h-2 p-0" colspan="4"></td>
960
+ <td class="h-2 p-0" colspan="5"></td>
920
961
  </tr>
921
962
  ${webhooks.map(
922
963
  (item) => html`
@@ -955,6 +996,16 @@ ${jsonPretty(item.gatewayBody)}</pre
955
996
  title=${item.health}
956
997
  />
957
998
  </td>
999
+ <td
1000
+ class="px-3 py-2.5 text-xs text-gray-400 group-hover:bg-white/5 transition-colors"
1001
+ >
1002
+ ${item.managed
1003
+ ? html`<span
1004
+ class="inline-flex items-center rounded-full px-2 py-0.5 text-[11px] bg-cyan-500/10 text-cyan-200"
1005
+ >Managed</span
1006
+ >`
1007
+ : html`<${Badge} tone="neutral">Custom</${Badge}>`}
1008
+ </td>
958
1009
  </tr>
959
1010
  `,
960
1011
  )}
@@ -976,7 +1027,9 @@ ${jsonPretty(item.gatewayBody)}</pre
976
1027
  onClose=${() => setIsCreating(false)}
977
1028
  />
978
1029
  <${ConfirmDialog}
979
- visible=${showDeleteConfirm && !!selectedHookName}
1030
+ visible=${showDeleteConfirm &&
1031
+ !!selectedHookName &&
1032
+ !selectedWebhookManaged}
980
1033
  title="Delete webhook?"
981
1034
  message=${`This removes "/hooks/${selectedHookName}" from openclaw.json.`}
982
1035
  details=${html`