@chrysb/alphaclaw 0.5.6 → 0.5.7-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 (84) hide show
  1. package/bin/alphaclaw.js +6 -1
  2. package/lib/public/css/agents.css +92 -0
  3. package/lib/public/css/explorer.css +101 -0
  4. package/lib/public/css/shell.css +15 -4
  5. package/lib/public/js/app.js +69 -3
  6. package/lib/public/js/components/action-button.js +5 -0
  7. package/lib/public/js/components/agents-tab/agent-bindings-section/helpers.js +76 -0
  8. package/lib/public/js/components/agents-tab/agent-bindings-section/index.js +490 -0
  9. package/lib/public/js/components/agents-tab/agent-bindings-section/use-agent-bindings.js +256 -0
  10. package/lib/public/js/components/agents-tab/agent-detail-panel.js +74 -0
  11. package/lib/public/js/components/agents-tab/agent-identity-section.js +175 -0
  12. package/lib/public/js/components/agents-tab/agent-overview/index.js +53 -0
  13. package/lib/public/js/components/agents-tab/agent-overview/manage-card.js +44 -0
  14. package/lib/public/js/components/agents-tab/agent-overview/model-card.js +158 -0
  15. package/lib/public/js/components/agents-tab/agent-overview/use-model-card.js +169 -0
  16. package/lib/public/js/components/agents-tab/agent-overview/use-workspace-card.js +45 -0
  17. package/lib/public/js/components/agents-tab/agent-overview/workspace-card.js +47 -0
  18. package/lib/public/js/components/agents-tab/agent-pairing-section.js +265 -0
  19. package/lib/public/js/components/agents-tab/create-agent-modal.js +189 -0
  20. package/lib/public/js/components/agents-tab/create-channel-modal.js +323 -0
  21. package/lib/public/js/components/agents-tab/delete-agent-dialog.js +50 -0
  22. package/lib/public/js/components/agents-tab/edit-agent-modal.js +109 -0
  23. package/lib/public/js/components/agents-tab/index.js +148 -0
  24. package/lib/public/js/components/agents-tab/use-agents.js +89 -0
  25. package/lib/public/js/components/channel-account-status-badge.js +35 -0
  26. package/lib/public/js/components/channel-operations-panel.js +33 -0
  27. package/lib/public/js/components/channels.js +545 -60
  28. package/lib/public/js/components/envars.js +25 -4
  29. package/lib/public/js/components/general/index.js +21 -11
  30. package/lib/public/js/components/general/use-general-tab.js +78 -16
  31. package/lib/public/js/components/google/gmail-setup-wizard.js +1 -3
  32. package/lib/public/js/components/google/index.js +28 -30
  33. package/lib/public/js/components/icons.js +37 -0
  34. package/lib/public/js/components/models-tab/index.js +58 -224
  35. package/lib/public/js/components/models-tab/model-picker.js +212 -0
  36. package/lib/public/js/components/models-tab/use-models.js +17 -14
  37. package/lib/public/js/components/onboarding/use-welcome-pairing.js +4 -4
  38. package/lib/public/js/components/onboarding/welcome-pairing-step.js +2 -2
  39. package/lib/public/js/components/overflow-menu.js +122 -0
  40. package/lib/public/js/components/pairings.js +36 -8
  41. package/lib/public/js/components/routes/agents-route.js +27 -0
  42. package/lib/public/js/components/routes/general-route.js +2 -0
  43. package/lib/public/js/components/routes/index.js +1 -0
  44. package/lib/public/js/components/routes/telegram-route.js +2 -2
  45. package/lib/public/js/components/secret-input.js +8 -1
  46. package/lib/public/js/components/sidebar.js +64 -26
  47. package/lib/public/js/components/telegram-workspace/index.js +175 -74
  48. package/lib/public/js/components/telegram-workspace/manage.js +83 -10
  49. package/lib/public/js/components/telegram-workspace/onboarding.js +9 -8
  50. package/lib/public/js/components/webhooks.js +43 -18
  51. package/lib/public/js/hooks/use-app-shell-controller.js +7 -0
  52. package/lib/public/js/hooks/use-browse-navigation.js +8 -5
  53. package/lib/public/js/hooks/use-destination-session-selection.js +8 -1
  54. package/lib/public/js/lib/api.js +163 -9
  55. package/lib/public/js/lib/app-navigation.js +2 -1
  56. package/lib/public/js/lib/channel-create-operation.js +102 -0
  57. package/lib/public/js/lib/format.js +14 -0
  58. package/lib/public/js/lib/sse.js +51 -0
  59. package/lib/public/js/lib/telegram-api.js +38 -18
  60. package/lib/public/setup.html +1 -0
  61. package/lib/public/shared/browse-file-policies.json +0 -1
  62. package/lib/server/agents/service.js +1478 -0
  63. package/lib/server/constants.js +2 -2
  64. package/lib/server/env.js +3 -1
  65. package/lib/server/gateway.js +104 -20
  66. package/lib/server/gmail-watch.js +29 -2
  67. package/lib/server/onboarding/import/import-applier.js +0 -1
  68. package/lib/server/onboarding/index.js +0 -6
  69. package/lib/server/onboarding/workspace.js +73 -38
  70. package/lib/server/openclaw-config.js +23 -0
  71. package/lib/server/operation-events.js +141 -0
  72. package/lib/server/routes/agents.js +266 -0
  73. package/lib/server/routes/pairings.js +135 -25
  74. package/lib/server/routes/system.js +90 -10
  75. package/lib/server/routes/telegram.js +247 -51
  76. package/lib/server/telegram-workspace.js +61 -10
  77. package/lib/server/topic-registry.js +66 -7
  78. package/lib/server/watchdog.js +39 -1
  79. package/lib/server/webhooks.js +60 -12
  80. package/lib/server.js +21 -7
  81. package/lib/setup/core-prompts/AGENTS.md +6 -5
  82. package/lib/setup/core-prompts/TOOLS.md +1 -8
  83. package/package.json +1 -1
  84. package/lib/setup/skills/control-ui/SKILL.md +0 -62
@@ -0,0 +1,490 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ import { ActionButton } from "../../action-button.js";
5
+ import { Badge } from "../../badge.js";
6
+ import { ChannelAccountStatusBadge } from "../../channel-account-status-badge.js";
7
+ import { ALL_CHANNELS, ChannelsCard, getChannelMeta } from "../../channels.js";
8
+ import { ConfirmDialog } from "../../confirm-dialog.js";
9
+ import { AddLineIcon } from "../../icons.js";
10
+ import { OverflowMenu, OverflowMenuItem } from "../../overflow-menu.js";
11
+ import { CreateChannelModal } from "../create-channel-modal.js";
12
+ import {
13
+ canAgentBindAccount,
14
+ getChannelItemSortRank,
15
+ getResolvedAccountStatusInfo,
16
+ isImplicitDefaultAccount,
17
+ resolveChannelAccountLabel,
18
+ } from "./helpers.js";
19
+ import { useAgentBindings } from "./use-agent-bindings.js";
20
+
21
+ const html = htm.bind(h);
22
+
23
+ export const AgentBindingsSection = ({
24
+ agent = {},
25
+ agents = [],
26
+ onSetLocation = () => {},
27
+ }) => {
28
+ const {
29
+ agentId,
30
+ agentNameMap,
31
+ channelStatus,
32
+ channels,
33
+ configuredChannelMap,
34
+ configuredChannels,
35
+ createLoadingLabel,
36
+ createProvider,
37
+ defaultAgentId,
38
+ deletingAccount,
39
+ editingAccount,
40
+ handleCreateChannel,
41
+ handleDeleteChannel,
42
+ handleQuickBind,
43
+ handleUpdateChannel,
44
+ isDefaultAgent,
45
+ loading,
46
+ menuOpenId,
47
+ openCreateChannelModal,
48
+ openDeleteChannelDialog,
49
+ openEditChannelModal,
50
+ pendingBindAccount,
51
+ requestBindAccount,
52
+ saving,
53
+ setCreateProvider,
54
+ setDeletingAccount,
55
+ setEditingAccount,
56
+ setMenuOpenId,
57
+ setPendingBindAccount,
58
+ setShowCreateModal,
59
+ showCreateModal,
60
+ } = useAgentBindings({ agent, agents });
61
+ const hasDiscordAccount = useMemo(() => {
62
+ const discordChannel = configuredChannelMap.get("discord");
63
+ return Array.isArray(discordChannel?.accounts) && discordChannel.accounts.length > 0;
64
+ }, [configuredChannelMap]);
65
+ const [showAssignedElsewhere, setShowAssignedElsewhere] = useState(false);
66
+
67
+ const channelItems = useMemo(() => {
68
+ const channelOrderMap = new Map(
69
+ configuredChannels.map((entry, index) => [
70
+ String(entry?.channel || "").trim(),
71
+ index,
72
+ ]),
73
+ );
74
+ const accountOrderMap = new Map(
75
+ configuredChannels.flatMap((entry) =>
76
+ (Array.isArray(entry?.accounts) ? entry.accounts : []).map(
77
+ (account, accountIndex) => [
78
+ `${String(entry?.channel || "").trim()}:${String(account?.id || "").trim() || "default"}`,
79
+ accountIndex,
80
+ ],
81
+ ),
82
+ ),
83
+ );
84
+ const channelIds = Array.from(
85
+ new Set([
86
+ ...configuredChannels.map((entry) => String(entry.channel || "").trim()),
87
+ ]),
88
+ ).filter(Boolean);
89
+
90
+ return channelIds
91
+ .flatMap((channelId) => {
92
+ const configuredChannel = configuredChannelMap.get(channelId);
93
+ const statusInfo = channelStatus?.[channelId] || null;
94
+ const accounts = Array.isArray(configuredChannel?.accounts)
95
+ ? configuredChannel.accounts
96
+ : [];
97
+
98
+ if (!configuredChannel && !statusInfo) return [];
99
+
100
+ return accounts.map((account) => {
101
+ const accountId = String(account?.id || "").trim() || "default";
102
+ const boundAgentId = String(account?.boundAgentId || "").trim();
103
+ const accountStatusInfo = getResolvedAccountStatusInfo({
104
+ account,
105
+ statusInfo,
106
+ accountId,
107
+ });
108
+ const isImplicitDefaultOwned =
109
+ isDefaultAgent &&
110
+ isImplicitDefaultAccount({ accountId, boundAgentId });
111
+ const isOwned = boundAgentId === agentId || isImplicitDefaultOwned;
112
+ const isImplicitDefaultElsewhere =
113
+ !isDefaultAgent &&
114
+ isImplicitDefaultAccount({ accountId, boundAgentId });
115
+ const isAvailable = canAgentBindAccount({
116
+ accountId,
117
+ boundAgentId,
118
+ agentId,
119
+ isDefaultAgent,
120
+ });
121
+ const ownerAgentId =
122
+ boundAgentId ||
123
+ (isImplicitDefaultAccount({ accountId, boundAgentId })
124
+ ? defaultAgentId
125
+ : "");
126
+ const ownerAgentName = String(
127
+ agentNameMap.get(ownerAgentId) || ownerAgentId || "",
128
+ ).trim();
129
+ const canNavigateToOwnerAgent =
130
+ !!ownerAgentId && ownerAgentId !== agentId && !!ownerAgentName;
131
+ const canOpenWorkspace =
132
+ channelId === "telegram" &&
133
+ isOwned &&
134
+ accountStatusInfo?.status === "paired";
135
+
136
+ const accountData = {
137
+ id: accountId,
138
+ provider: channelId,
139
+ name: resolveChannelAccountLabel({ channelId, account }),
140
+ rawName: String(account?.name || "").trim(),
141
+ ownerAgentId,
142
+ ownerAgentName,
143
+ boundAgentId,
144
+ isOwned,
145
+ envKey: String(account?.envKey || "").trim(),
146
+ token: String(account?.token || "").trim(),
147
+ isAvailable,
148
+ isBoundElsewhere:
149
+ !isOwned &&
150
+ (!isAvailable || isImplicitDefaultElsewhere || !!ownerAgentId),
151
+ };
152
+ let statusTrailing = null;
153
+ if (isOwned) {
154
+ statusTrailing =
155
+ accountStatusInfo?.status === "paired"
156
+ ? html`<${ChannelAccountStatusBadge}
157
+ status=${accountStatusInfo?.status}
158
+ ownerAgentName=${ownerAgentName}
159
+ showAgentBadge=${true}
160
+ channelId=${channelId}
161
+ pairedCount=${accountStatusInfo?.paired ?? 0}
162
+ />`
163
+ : html`<${ChannelAccountStatusBadge}
164
+ status=${accountStatusInfo?.status}
165
+ ownerAgentName=""
166
+ showAgentBadge=${false}
167
+ channelId=${channelId}
168
+ pairedCount=${accountStatusInfo?.paired ?? 0}
169
+ />`;
170
+ } else if (isAvailable) {
171
+ statusTrailing = html`
172
+ <button
173
+ type="button"
174
+ onclick=${(event) => {
175
+ event.stopPropagation();
176
+ requestBindAccount(accountData);
177
+ }}
178
+ class="text-xs px-2 py-1 rounded-lg ac-btn-ghost"
179
+ >
180
+ Bind
181
+ </button>
182
+ `;
183
+ } else {
184
+ statusTrailing = html`
185
+ ${canNavigateToOwnerAgent
186
+ ? html`
187
+ <button
188
+ type="button"
189
+ class="inline-flex rounded-full transition-[filter] hover:brightness-125 focus:outline-none focus:ring-1 focus:ring-border"
190
+ onclick=${(event) => {
191
+ event.stopPropagation();
192
+ onSetLocation(`/agents/${encodeURIComponent(ownerAgentId)}`);
193
+ }}
194
+ title=${`Open ${ownerAgentName}`}
195
+ aria-label=${`Open ${ownerAgentName}`}
196
+ >
197
+ <${Badge} tone="neutral">${ownerAgentName}</${Badge}>
198
+ </button>
199
+ `
200
+ : html`<${Badge} tone="neutral">${ownerAgentName || "Bound elsewhere"}</${Badge}>`}
201
+ `;
202
+ }
203
+
204
+ const showBindAction = accountData.isBoundElsewhere;
205
+ const canEditOrDelete = !accountData.isBoundElsewhere;
206
+ const accountTrailing = html`
207
+ <div class="flex items-center gap-1.5">
208
+ ${statusTrailing}
209
+ <${OverflowMenu}
210
+ open=${menuOpenId === `${channelId}:${accountId}`}
211
+ ariaLabel="Open channel actions"
212
+ title="Open channel actions"
213
+ onClose=${() => setMenuOpenId("")}
214
+ onToggle=${() =>
215
+ setMenuOpenId((current) =>
216
+ current === `${channelId}:${accountId}`
217
+ ? ""
218
+ : `${channelId}:${accountId}`,
219
+ )}
220
+ >
221
+ ${canEditOrDelete
222
+ ? html`
223
+ <${OverflowMenuItem}
224
+ onClick=${() => openEditChannelModal(accountData)}
225
+ >
226
+ Edit
227
+ </${OverflowMenuItem}>
228
+ `
229
+ : null}
230
+ ${showBindAction
231
+ ? html`
232
+ <${OverflowMenuItem}
233
+ onClick=${() => requestBindAccount(accountData)}
234
+ >
235
+ Bind
236
+ </${OverflowMenuItem}>
237
+ `
238
+ : null}
239
+ ${canEditOrDelete
240
+ ? html`
241
+ <${OverflowMenuItem}
242
+ className="text-red-300 hover:text-red-200"
243
+ onClick=${() => openDeleteChannelDialog(accountData)}
244
+ >
245
+ Delete
246
+ </${OverflowMenuItem}>
247
+ `
248
+ : null}
249
+ </${OverflowMenu}>
250
+ </div>
251
+ `;
252
+
253
+ return {
254
+ id: `${channelId}:${accountId}`,
255
+ channel: channelId,
256
+ channelOrder: Number(channelOrderMap.get(channelId) ?? 9999),
257
+ accountOrder: Number(
258
+ accountOrderMap.get(`${channelId}:${accountId}`) ?? 9999,
259
+ ),
260
+ label: resolveChannelAccountLabel({ channelId, account }),
261
+ isAwaitingPairing: accountStatusInfo?.status !== "paired",
262
+ clickable: canOpenWorkspace,
263
+ onClick: canOpenWorkspace ? () => onSetLocation(`/telegram/${encodeURIComponent(account?.id || "default")}`) : undefined,
264
+ detailText: canOpenWorkspace ? "Workspace" : "",
265
+ detailChevron: canOpenWorkspace,
266
+ trailing: accountTrailing,
267
+ isOwned,
268
+ isAvailable,
269
+ dimmedLabel: accountData.isBoundElsewhere,
270
+ isBoundElsewhere: accountData.isBoundElsewhere,
271
+ };
272
+ });
273
+ })
274
+ .filter(Boolean)
275
+ .sort((a, b) => {
276
+ const rankDiff = getChannelItemSortRank(a) - getChannelItemSortRank(b);
277
+ if (rankDiff !== 0) return rankDiff;
278
+ const channelOrderDiff =
279
+ Number(a?.channelOrder ?? 9999) - Number(b?.channelOrder ?? 9999);
280
+ if (channelOrderDiff !== 0) return channelOrderDiff;
281
+ const accountOrderDiff =
282
+ Number(a?.accountOrder ?? 9999) - Number(b?.accountOrder ?? 9999);
283
+ if (accountOrderDiff !== 0) return accountOrderDiff;
284
+ return String(a?.label || "").localeCompare(String(b?.label || ""));
285
+ });
286
+ }, [
287
+ agentId,
288
+ agentNameMap,
289
+ channelStatus,
290
+ configuredChannelMap,
291
+ configuredChannels,
292
+ defaultAgentId,
293
+ isDefaultAgent,
294
+ menuOpenId,
295
+ onSetLocation,
296
+ openCreateChannelModal,
297
+ openDeleteChannelDialog,
298
+ openEditChannelModal,
299
+ requestBindAccount,
300
+ setMenuOpenId,
301
+ ]);
302
+ const visibleChannelItems = channelItems.filter(
303
+ (item) => !item?.isBoundElsewhere,
304
+ );
305
+ const assignedElsewhereItems = channelItems.filter(
306
+ (item) => !!item?.isBoundElsewhere,
307
+ );
308
+ useEffect(() => {
309
+ if (assignedElsewhereItems.length === 0) {
310
+ setShowAssignedElsewhere(false);
311
+ return;
312
+ }
313
+ if (visibleChannelItems.length === 0) {
314
+ setShowAssignedElsewhere(true);
315
+ }
316
+ }, [agentId, assignedElsewhereItems.length, visibleChannelItems.length]);
317
+ const mergedChannelItems = useMemo(() => {
318
+ const baseItems = [...visibleChannelItems];
319
+ if (assignedElsewhereItems.length === 0) return baseItems;
320
+ baseItems.push({
321
+ id: "__assigned_elsewhere_toggle",
322
+ label: html`
323
+ <span class="inline-flex items-center gap-1.5">
324
+ <span class=${`arrow inline-block ${showAssignedElsewhere ? "" : "-rotate-90"}`}>▼</span>
325
+ <span>Assigned elsewhere</span>
326
+ </span>
327
+ `,
328
+ labelClassName: "text-xs",
329
+ clickable: true,
330
+ onClick: () => setShowAssignedElsewhere((current) => !current),
331
+ dimmedLabel: true,
332
+ trailing: html`
333
+ <span class="inline-flex items-center gap-1.5 text-gray-500">
334
+ <span class="text-[11px] px-2 py-0.5 rounded-full border border-border">
335
+ ${assignedElsewhereItems.length}
336
+ </span>
337
+ </span>
338
+ `,
339
+ });
340
+ if (showAssignedElsewhere) {
341
+ baseItems.push(...assignedElsewhereItems);
342
+ }
343
+ return baseItems;
344
+ }, [assignedElsewhereItems, showAssignedElsewhere, visibleChannelItems]);
345
+
346
+ return html`
347
+ <div class="space-y-3">
348
+ ${loading
349
+ ? html`
350
+ <${ChannelsCard}
351
+ title="Channels"
352
+ items=${[]}
353
+ loadingLabel="Loading channels..."
354
+ actions=${html`
355
+ <div class="relative">
356
+ <${ActionButton}
357
+ onClick=${() => {}}
358
+ disabled=${true}
359
+ tone="subtle"
360
+ size="sm"
361
+ idleIcon=${AddLineIcon}
362
+ idleIconClassName="h-3.5 w-3.5"
363
+ iconOnly=${true}
364
+ title="Add channel"
365
+ ariaLabel="Add channel"
366
+ idleLabel="Add channel"
367
+ />
368
+ </div>
369
+ `}
370
+ />
371
+ `
372
+ : html`
373
+ <div class="space-y-3">
374
+ <${ChannelsCard}
375
+ title="Channels"
376
+ items=${mergedChannelItems}
377
+ loadingLabel="No channels assigned to this agent."
378
+ actions=${html`
379
+ <${OverflowMenu}
380
+ open=${menuOpenId === "__create_channel"}
381
+ ariaLabel="Add channel"
382
+ title="Add channel"
383
+ onClose=${() => setMenuOpenId("")}
384
+ onToggle=${() =>
385
+ setMenuOpenId((current) =>
386
+ current === "__create_channel" ? "" : "__create_channel",
387
+ )}
388
+ renderTrigger=${({ onToggle, ariaLabel, title }) => html`
389
+ <${ActionButton}
390
+ onClick=${onToggle}
391
+ disabled=${saving}
392
+ loading=${false}
393
+ loadingMode="inline"
394
+ tone="subtle"
395
+ size="sm"
396
+ loadingLabel="Opening..."
397
+ idleIcon=${AddLineIcon}
398
+ idleIconClassName="h-3.5 w-3.5"
399
+ iconOnly=${true}
400
+ title=${title}
401
+ ariaLabel=${ariaLabel}
402
+ idleLabel="Add channel"
403
+ />
404
+ `}
405
+ >
406
+ ${ALL_CHANNELS.map((channelId) => {
407
+ const channelMeta = getChannelMeta(channelId);
408
+ const isDisabled = channelId === "discord" && hasDiscordAccount;
409
+ return html`
410
+ <${OverflowMenuItem}
411
+ key=${channelId}
412
+ iconSrc=${channelMeta.iconSrc}
413
+ disabled=${isDisabled}
414
+ onClick=${() => openCreateChannelModal(channelId)}
415
+ >
416
+ ${channelMeta.label}
417
+ </${OverflowMenuItem}>
418
+ `;
419
+ })}
420
+ </${OverflowMenu}>
421
+ `}
422
+ />
423
+ </div>
424
+ `}
425
+ <${CreateChannelModal}
426
+ visible=${showCreateModal}
427
+ loading=${saving}
428
+ createLoadingLabel=${createLoadingLabel}
429
+ agents=${agents}
430
+ existingChannels=${channels}
431
+ initialAgentId=${agentId}
432
+ initialProvider=${createProvider}
433
+ onClose=${() => {
434
+ setShowCreateModal(false);
435
+ setCreateProvider("");
436
+ }}
437
+ onSubmit=${handleCreateChannel}
438
+ />
439
+ <${CreateChannelModal}
440
+ visible=${!!editingAccount}
441
+ loading=${saving}
442
+ agents=${agents}
443
+ existingChannels=${channels}
444
+ mode="edit"
445
+ account=${editingAccount}
446
+ initialAgentId=${String(editingAccount?.ownerAgentId || agentId || "").trim()}
447
+ initialProvider=${String(editingAccount?.provider || "").trim()}
448
+ onClose=${() => setEditingAccount(null)}
449
+ onSubmit=${handleUpdateChannel}
450
+ />
451
+ <${ConfirmDialog}
452
+ visible=${!!pendingBindAccount}
453
+ title=${`Bind ${String(pendingBindAccount?.name || "this channel").trim()} to ${String(agent?.name || agentId).trim()}?`}
454
+ message=""
455
+ details=${pendingBindAccount
456
+ ? html`
457
+ <p class="text-xs text-gray-500">
458
+ This will remove access for ${String(
459
+ pendingBindAccount?.ownerAgentName || "the other agent",
460
+ ).trim()} to this channel.
461
+ </p>
462
+ `
463
+ : null}
464
+ confirmLabel="Bind channel"
465
+ confirmLoadingLabel="Binding..."
466
+ confirmTone="warning"
467
+ confirmLoading=${saving}
468
+ onConfirm=${() => handleQuickBind(pendingBindAccount)}
469
+ onCancel=${() => {
470
+ if (saving) return;
471
+ setPendingBindAccount(null);
472
+ }}
473
+ />
474
+ <${ConfirmDialog}
475
+ visible=${!!deletingAccount}
476
+ title="Delete channel?"
477
+ message=${`Remove ${String(deletingAccount?.name || "this channel").trim()} from your configured channels?`}
478
+ confirmLabel="Delete"
479
+ confirmLoadingLabel="Deleting..."
480
+ confirmTone="warning"
481
+ confirmLoading=${saving}
482
+ onConfirm=${handleDeleteChannel}
483
+ onCancel=${() => {
484
+ if (saving) return;
485
+ setDeletingAccount(null);
486
+ }}
487
+ />
488
+ </div>
489
+ `;
490
+ };