@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,122 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useEffect, useRef } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ const VerticalDotsIcon = ({ className = "" }) => html`
8
+ <svg class=${className} width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
9
+ <circle cx="8" cy="3" r="1.5" />
10
+ <circle cx="8" cy="8" r="1.5" />
11
+ <circle cx="8" cy="13" r="1.5" />
12
+ </svg>
13
+ `;
14
+
15
+ export const OverflowMenu = ({
16
+ open = false,
17
+ onToggle = () => {},
18
+ onClose = () => {},
19
+ ariaLabel = "Open menu",
20
+ title = "",
21
+ menuRef = null,
22
+ renderTrigger = null,
23
+ triggerDisabled = false,
24
+ children = null,
25
+ }) => {
26
+ const internalMenuRef = useRef(null);
27
+ const setMenuNodeRef = (node) => {
28
+ internalMenuRef.current = node;
29
+ if (typeof menuRef === "function") {
30
+ menuRef(node);
31
+ return;
32
+ }
33
+ if (menuRef && typeof menuRef === "object") {
34
+ menuRef.current = node;
35
+ }
36
+ };
37
+
38
+ useEffect(() => {
39
+ if (!open) return undefined;
40
+ const handleWindowClick = (event) => {
41
+ const root = internalMenuRef.current;
42
+ if (!root) return;
43
+ if (root.contains(event.target)) return;
44
+ onClose(event);
45
+ };
46
+ window.addEventListener("click", handleWindowClick);
47
+ return () => window.removeEventListener("click", handleWindowClick);
48
+ }, [open, onClose]);
49
+
50
+ return html`
51
+ <div class="brand-menu" ref=${setMenuNodeRef}>
52
+ ${typeof renderTrigger === "function"
53
+ ? renderTrigger({
54
+ open,
55
+ onToggle: (event) => {
56
+ event.stopPropagation();
57
+ onToggle(event);
58
+ },
59
+ ariaLabel,
60
+ title: title || ariaLabel,
61
+ })
62
+ : html`
63
+ <button
64
+ type="button"
65
+ class="brand-menu-trigger"
66
+ aria-label=${ariaLabel}
67
+ aria-expanded=${open ? "true" : "false"}
68
+ title=${title || ariaLabel}
69
+ disabled=${triggerDisabled}
70
+ onclick=${(event) => {
71
+ event.stopPropagation();
72
+ onToggle(event);
73
+ }}
74
+ >
75
+ <${VerticalDotsIcon} />
76
+ </button>
77
+ `}
78
+ ${open
79
+ ? html`
80
+ <div class="brand-dropdown" onclick=${(event) => event.stopPropagation()}>
81
+ ${children}
82
+ </div>
83
+ `
84
+ : null}
85
+ </div>
86
+ `;
87
+ };
88
+
89
+ export const OverflowMenuItem = ({
90
+ children = null,
91
+ onClick = () => {},
92
+ className = "",
93
+ iconSrc = "",
94
+ disabled = false,
95
+ }) => html`
96
+ <button
97
+ type="button"
98
+ class=${`brand-dropdown-item ${className} ${disabled
99
+ ? "opacity-50 cursor-not-allowed"
100
+ : ""}`.trim()}
101
+ disabled=${disabled}
102
+ onclick=${(event) => {
103
+ event.stopPropagation();
104
+ if (disabled) return;
105
+ onClick(event);
106
+ }}
107
+ >
108
+ ${iconSrc
109
+ ? html`
110
+ <span class="flex w-full items-center gap-2 leading-none">
111
+ <img
112
+ src=${iconSrc}
113
+ alt=""
114
+ class="block w-4 h-4 rounded-sm"
115
+ aria-hidden="true"
116
+ />
117
+ <span>${children}</span>
118
+ </span>
119
+ `
120
+ : children}
121
+ </button>
122
+ `;
@@ -4,39 +4,45 @@ import htm from 'https://esm.sh/htm';
4
4
  import { ActionButton } from './action-button.js';
5
5
  const html = htm.bind(h);
6
6
 
7
- const PairingRow = ({ p, onApprove, onReject }) => {
7
+ export const PairingRow = ({ p, onApprove, onReject }) => {
8
8
  const [busy, setBusy] = useState(null);
9
9
 
10
10
  const handle = async (action) => {
11
11
  setBusy(action);
12
12
  try {
13
- if (action === "approve") await onApprove(p.id, p.channel);
14
- else await onReject(p.id, p.channel);
13
+ if (action === "approve") await onApprove(p.id, p.channel, p.accountId);
14
+ else await onReject(p.id, p.channel, p.accountId);
15
15
  } catch {
16
16
  setBusy(null);
17
17
  }
18
18
  };
19
19
 
20
20
  const label = (p.channel || 'unknown').charAt(0).toUpperCase() + (p.channel || '').slice(1);
21
+ const accountId = String(p.accountId || "").trim();
22
+ const accountName = String(p.accountName || "").trim();
23
+ const accountSuffix =
24
+ accountId && accountId !== "default"
25
+ ? ` · ${accountName || accountId}`
26
+ : "";
21
27
 
22
28
  if (busy === "approve") {
23
29
  return html`
24
30
  <div class="bg-black/30 rounded-lg p-3 mb-2 flex items-center gap-2">
25
31
  <span class="text-green-400 text-sm">Approved</span>
26
- <span class="text-gray-500 text-xs">${label} · ${p.code || p.id || '?'}</span>
32
+ <span class="text-gray-500 text-xs">${label}${accountSuffix} · ${p.code || p.id || '?'}</span>
27
33
  </div>`;
28
34
  }
29
35
  if (busy === "reject") {
30
36
  return html`
31
37
  <div class="bg-black/30 rounded-lg p-3 mb-2 flex items-center gap-2">
32
38
  <span class="text-gray-400 text-sm">Rejected</span>
33
- <span class="text-gray-500 text-xs">${label} · ${p.code || p.id || '?'}</span>
39
+ <span class="text-gray-500 text-xs">${label}${accountSuffix} · ${p.code || p.id || '?'}</span>
34
40
  </div>`;
35
41
  }
36
42
 
37
43
  return html`
38
44
  <div class="bg-black/30 rounded-lg p-3 mb-2">
39
- <div class="font-medium text-sm mb-2">${label} · <code class="text-gray-400">${p.code || p.id || '?'}</code></div>
45
+ <div class="font-medium text-sm mb-2">${label}${accountSuffix} · <code class="text-gray-400">${p.code || p.id || '?'}</code></div>
40
46
  <div class="flex gap-2">
41
47
  <${ActionButton}
42
48
  onClick=${() => handle("approve")}
@@ -60,11 +66,29 @@ const ALL_CHANNELS = ['telegram', 'discord'];
60
66
 
61
67
  const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
62
68
 
63
- export function Pairings({ pending, channels, visible, onApprove, onReject }) {
69
+ export function Pairings({
70
+ pending,
71
+ channels,
72
+ visible,
73
+ onApprove,
74
+ onReject,
75
+ statusRefreshing = false,
76
+ }) {
64
77
  if (!visible) return null;
65
78
 
66
79
  const unpaired = ALL_CHANNELS
67
- .filter((ch) => channels?.[ch] && channels[ch].status !== 'paired')
80
+ .filter((ch) => {
81
+ const info = channels?.[ch];
82
+ if (!info) return false;
83
+ const accounts =
84
+ info.accounts && typeof info.accounts === "object" ? info.accounts : {};
85
+ if (Object.keys(accounts).length > 0) {
86
+ return Object.values(accounts).some(
87
+ (acc) => acc && acc.status !== "paired",
88
+ );
89
+ }
90
+ return info.status !== "paired";
91
+ })
68
92
  .map(capitalize);
69
93
 
70
94
  const channelList = unpaired.length <= 2
@@ -78,6 +102,10 @@ export function Pairings({ pending, channels, visible, onApprove, onReject }) {
78
102
  ? html`<div>
79
103
  ${pending.map(p => html`<${PairingRow} key=${p.id} p=${p} onApprove=${onApprove} onReject=${onReject} />`)}
80
104
  </div>`
105
+ : statusRefreshing
106
+ ? html`<div class="text-center py-4 space-y-2">
107
+ <p class="text-gray-300 text-sm">Updating pairing status...</p>
108
+ </div>`
81
109
  : html`<div class="text-center py-4 space-y-2">
82
110
  <div class="text-3xl">💬</div>
83
111
  <p class="text-gray-300 text-sm">Send a message to your bot on ${channelList}</p>
@@ -0,0 +1,27 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { AgentsTab } from "../agents-tab/index.js";
4
+
5
+ const html = htm.bind(h);
6
+
7
+ export const AgentsRoute = ({
8
+ agents = [],
9
+ loading = false,
10
+ saving = false,
11
+ agentsActions = {},
12
+ selectedAgentId = "",
13
+ onSelectAgent = () => {},
14
+ onNavigateToBrowseFile = () => {},
15
+ onSetLocation = () => {},
16
+ }) => html`
17
+ <${AgentsTab}
18
+ agents=${agents}
19
+ loading=${loading}
20
+ saving=${saving}
21
+ agentsActions=${agentsActions}
22
+ selectedAgentId=${selectedAgentId}
23
+ onSelectAgent=${onSelectAgent}
24
+ onNavigateToBrowseFile=${onNavigateToBrowseFile}
25
+ onSetLocation=${onSetLocation}
26
+ />
27
+ `;
@@ -8,6 +8,7 @@ export const GeneralRoute = ({
8
8
  statusData = null,
9
9
  watchdogData = null,
10
10
  doctorStatusData = null,
11
+ agents = [],
11
12
  doctorWarningDismissedUntilMs = 0,
12
13
  onRefreshStatuses = () => {},
13
14
  onSetLocation = () => {},
@@ -26,6 +27,7 @@ export const GeneralRoute = ({
26
27
  statusData=${statusData}
27
28
  watchdogData=${watchdogData}
28
29
  doctorStatusData=${doctorStatusData}
30
+ agents=${agents}
29
31
  doctorWarningDismissedUntilMs=${doctorWarningDismissedUntilMs}
30
32
  onRefreshStatuses=${onRefreshStatuses}
31
33
  onSwitchTab=${(nextTab) => onSetLocation(`/${nextTab}`)}
@@ -1,3 +1,4 @@
1
+ export { AgentsRoute } from "./agents-route.js";
1
2
  export { BrowseRoute } from "./browse-route.js";
2
3
  export { DoctorRoute } from "./doctor-route.js";
3
4
  export { EnvarsRoute } from "./envars-route.js";
@@ -4,8 +4,8 @@ import { TelegramWorkspace } from "../telegram-workspace/index.js";
4
4
 
5
5
  const html = htm.bind(h);
6
6
 
7
- export const TelegramRoute = ({ onBack = () => {} }) => html`
7
+ export const TelegramRoute = ({ accountId = "default", onBack = () => {} }) => html`
8
8
  <div class="pt-4">
9
- <${TelegramWorkspace} onBack=${onBack} />
9
+ <${TelegramWorkspace} key=${accountId} accountId=${accountId} onBack=${onBack} />
10
10
  </div>
11
11
  `;
@@ -1,6 +1,7 @@
1
1
  import { h } from "https://esm.sh/preact";
2
2
  import { useState } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
+ import { LoadingSpinner } from "./loading-spinner.js";
4
5
  const html = htm.bind(h);
5
6
 
6
7
  /**
@@ -17,10 +18,12 @@ export const SecretInput = ({
17
18
  placeholder = "",
18
19
  inputClass = "",
19
20
  disabled = false,
21
+ loading = false,
20
22
  isSecret = true,
21
23
  }) => {
22
24
  const [visible, setVisible] = useState(false);
23
25
  const showToggle = isSecret;
26
+ const isDisabled = disabled || loading;
24
27
 
25
28
  return html`
26
29
  <div class="flex-1 min-w-0 flex items-center gap-1">
@@ -30,14 +33,18 @@ export const SecretInput = ({
30
33
  placeholder=${placeholder}
31
34
  onInput=${onInput}
32
35
  onBlur=${onBlur}
33
- disabled=${disabled}
36
+ disabled=${isDisabled}
34
37
  class=${inputClass}
35
38
  autocomplete="off"
36
39
  />
40
+ ${loading
41
+ ? html`<${LoadingSpinner} className="h-3 w-3 text-gray-500 shrink-0" />`
42
+ : null}
37
43
  ${showToggle
38
44
  ? html`<button
39
45
  type="button"
40
46
  onclick=${() => setVisible((v) => !v)}
47
+ disabled=${isDisabled}
41
48
  class="text-gray-500 hover:text-gray-300 px-1 text-xs shrink-0"
42
49
  >
43
50
  ${visible ? "Hide" : "Show"}
@@ -1,8 +1,9 @@
1
1
  import { h } from "https://esm.sh/preact";
2
2
  import { useEffect, useRef, useState } from "https://esm.sh/preact/hooks";
3
3
  import htm from "https://esm.sh/htm";
4
- import { HomeLineIcon, FolderLineIcon } from "./icons.js";
4
+ import { AddLineIcon, HomeLineIcon, FolderLineIcon } from "./icons.js";
5
5
  import { FileTree } from "./file-tree.js";
6
+ import { OverflowMenu, OverflowMenuItem } from "./overflow-menu.js";
6
7
  import { UpdateActionButton } from "./update-action-button.js";
7
8
  import { SidebarGitPanel } from "./sidebar-git-panel.js";
8
9
  import { readUiSettings, writeUiSettings } from "../lib/ui-settings.js";
@@ -49,6 +50,10 @@ export const AppSidebar = ({
49
50
  acLatest = "",
50
51
  acUpdating = false,
51
52
  onAcUpdate = () => {},
53
+ agents = [],
54
+ selectedAgentId = "",
55
+ onSelectAgent = () => {},
56
+ onAddAgent = () => {},
52
57
  }) => {
53
58
  const browseLayoutRef = useRef(null);
54
59
  const browseBottomPanelRef = useRef(null);
@@ -123,36 +128,27 @@ export const AppSidebar = ({
123
128
  setIsResizingBrowsePanels(true);
124
129
  };
125
130
 
131
+ const setupSection = navSections.find((section) => section.label === "Setup") || null;
132
+ const remainingSections = navSections.filter((section) => section.label !== "Setup");
133
+
126
134
  return html`
127
135
  <div class=${`app-sidebar ${mobileSidebarOpen ? "mobile-open" : ""}`}>
128
136
  <div class="sidebar-brand">
129
137
  <img src="./img/logo.svg" alt="" width="20" height="20" />
130
138
  <span><span style="color: var(--accent)">alpha</span>claw</span>
131
139
  ${authEnabled && html`
132
- <div class="brand-menu" ref=${menuRef}>
133
- <button
134
- class="brand-menu-trigger"
135
- onclick=${onToggleMenu}
136
- aria-label="Menu"
137
- >
138
- <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
139
- <circle cx="8" cy="3" r="1.5" />
140
- <circle cx="8" cy="8" r="1.5" />
141
- <circle cx="8" cy="13" r="1.5" />
142
- </svg>
143
- </button>
144
- ${menuOpen && html`
145
- <div class="brand-dropdown">
146
- <a
147
- href="#"
148
- onclick=${(event) => {
149
- event.preventDefault();
150
- onLogout();
151
- }}
152
- >Log out</a>
153
- </div>
154
- `}
155
- </div>
140
+ <${OverflowMenu}
141
+ open=${menuOpen}
142
+ onToggle=${onToggleMenu}
143
+ onClose=${onToggleMenu}
144
+ ariaLabel="Menu"
145
+ title="Menu"
146
+ menuRef=${menuRef}
147
+ >
148
+ <${OverflowMenuItem} onClick=${() => onLogout()}>
149
+ Log out
150
+ </${OverflowMenuItem}>
151
+ </${OverflowMenu}>
156
152
  `}
157
153
  </div>
158
154
  <div class="sidebar-tabs">
@@ -181,7 +177,49 @@ export const AppSidebar = ({
181
177
  minHeight: 0,
182
178
  }}
183
179
  >
184
- ${navSections.map(
180
+ ${setupSection
181
+ ? html`
182
+ <div class="sidebar-label">Menu</div>
183
+ <nav class="sidebar-nav">
184
+ ${setupSection.items.map(
185
+ (item) => html`
186
+ <a
187
+ class=${selectedNavId === item.id ? "active" : ""}
188
+ onclick=${() => onSelectNavItem(item.id)}
189
+ >
190
+ ${item.label}
191
+ </a>
192
+ `,
193
+ )}
194
+ </nav>
195
+ `
196
+ : null}
197
+ <div class="sidebar-agents-header">
198
+ <div class="sidebar-label sidebar-agents-label">Agents</div>
199
+ <button
200
+ type="button"
201
+ class="sidebar-agents-add-button"
202
+ onclick=${onAddAgent}
203
+ title="Add agent"
204
+ aria-label="Add agent"
205
+ >
206
+ <${AddLineIcon} className="sidebar-agents-add-icon" />
207
+ </button>
208
+ </div>
209
+ <div class="sidebar-agents-list">
210
+ ${agents.map(
211
+ (agent) => html`
212
+ <button
213
+ key=${agent.id}
214
+ class=${`sidebar-agent-item ${selectedAgentId === agent.id ? "active" : ""}`}
215
+ onclick=${() => onSelectAgent(agent.id)}
216
+ >
217
+ <span class="sidebar-agent-name">${agent.name || agent.id}</span>
218
+ </button>
219
+ `,
220
+ )}
221
+ </div>
222
+ ${remainingSections.map(
185
223
  (section) => html`
186
224
  <div class="sidebar-label">${section.label}</div>
187
225
  <nav class="sidebar-nav">