@chrysb/alphaclaw 0.6.1 → 0.6.2-beta.1

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 (50) hide show
  1. package/lib/public/css/agents.css +1 -1
  2. package/lib/public/css/cron.css +541 -0
  3. package/lib/public/css/theme.css +72 -0
  4. package/lib/public/js/app.js +45 -10
  5. package/lib/public/js/components/action-button.js +26 -20
  6. package/lib/public/js/components/agents-tab/agent-detail-panel.js +98 -17
  7. package/lib/public/js/components/agents-tab/agent-tools/index.js +105 -0
  8. package/lib/public/js/components/agents-tab/agent-tools/tool-catalog.js +289 -0
  9. package/lib/public/js/components/agents-tab/agent-tools/use-agent-tools.js +128 -0
  10. package/lib/public/js/components/agents-tab/index.js +4 -0
  11. package/lib/public/js/components/cron-tab/cron-calendar-helpers.js +385 -0
  12. package/lib/public/js/components/cron-tab/cron-calendar.js +441 -0
  13. package/lib/public/js/components/cron-tab/cron-helpers.js +326 -0
  14. package/lib/public/js/components/cron-tab/cron-job-detail.js +425 -0
  15. package/lib/public/js/components/cron-tab/cron-job-list.js +305 -0
  16. package/lib/public/js/components/cron-tab/cron-job-usage.js +70 -0
  17. package/lib/public/js/components/cron-tab/cron-overview.js +599 -0
  18. package/lib/public/js/components/cron-tab/cron-runs-trend-card.js +277 -0
  19. package/lib/public/js/components/cron-tab/index.js +100 -0
  20. package/lib/public/js/components/cron-tab/use-cron-tab.js +366 -0
  21. package/lib/public/js/components/doctor/summary-cards.js +5 -11
  22. package/lib/public/js/components/icons.js +13 -0
  23. package/lib/public/js/components/pill-tabs.js +33 -0
  24. package/lib/public/js/components/pop-actions.js +58 -0
  25. package/lib/public/js/components/routes/agents-route.js +4 -0
  26. package/lib/public/js/components/routes/cron-route.js +9 -0
  27. package/lib/public/js/components/routes/index.js +1 -0
  28. package/lib/public/js/components/segmented-control.js +15 -9
  29. package/lib/public/js/components/summary-stat-card.js +17 -0
  30. package/lib/public/js/components/tooltip.js +50 -4
  31. package/lib/public/js/components/watchdog-tab.js +46 -1
  32. package/lib/public/js/lib/api.js +94 -0
  33. package/lib/public/js/lib/app-navigation.js +2 -0
  34. package/lib/public/js/lib/storage-keys.js +1 -0
  35. package/lib/public/setup.html +1 -0
  36. package/lib/server/agents/agents.js +15 -0
  37. package/lib/server/constants.js +1 -0
  38. package/lib/server/cost-utils.js +312 -0
  39. package/lib/server/cron-service.js +461 -0
  40. package/lib/server/db/usage/index.js +131 -1
  41. package/lib/server/db/usage/pricing.js +1 -83
  42. package/lib/server/db/usage/sessions.js +4 -1
  43. package/lib/server/db/usage/shared.js +2 -1
  44. package/lib/server/db/usage/summary.js +5 -1
  45. package/lib/server/onboarding/index.js +39 -5
  46. package/lib/server/onboarding/openclaw.js +25 -19
  47. package/lib/server/onboarding/validation.js +28 -0
  48. package/lib/server/routes/cron.js +148 -0
  49. package/lib/server.js +13 -0
  50. package/package.json +1 -1
@@ -16,6 +16,7 @@ import { AppSidebar } from "./components/sidebar.js";
16
16
  import {
17
17
  AgentsRoute,
18
18
  BrowseRoute,
19
+ CronRoute,
19
20
  DoctorRoute,
20
21
  EnvarsRoute,
21
22
  GeneralRoute,
@@ -75,10 +76,20 @@ const App = () => {
75
76
  } = useAgents();
76
77
 
77
78
  const isAgentsRoute = location.startsWith("/agents");
79
+ const isCronRoute = location.startsWith("/cron");
78
80
  const selectedAgentId = (() => {
79
81
  const match = location.match(/^\/agents\/([^/]+)/);
80
82
  return match ? decodeURIComponent(match[1]) : "";
81
83
  })();
84
+ const agentDetailTab = (() => {
85
+ const match = location.match(/^\/agents\/[^/]+\/([^/]+)/);
86
+ const tab = match ? match[1] : "";
87
+ return tab === "tools" ? "tools" : "overview";
88
+ })();
89
+ const selectedCronJobId = (() => {
90
+ const match = location.match(/^\/cron\/([^/]+)/);
91
+ return match ? decodeURIComponent(match[1]) : "";
92
+ })();
82
93
 
83
94
  useEffect(() => {
84
95
  if (!isAgentsRoute) return;
@@ -233,15 +244,31 @@ const App = () => {
233
244
  saving=${agentsState.saving}
234
245
  agentsActions=${agentsActions}
235
246
  selectedAgentId=${selectedAgentId}
247
+ activeTab=${agentDetailTab}
236
248
  onSelectAgent=${(agentId) => setLocation(`/agents/${encodeURIComponent(agentId)}`)}
249
+ onSelectTab=${(tab) => {
250
+ const safePath = tab && tab !== "overview"
251
+ ? `/agents/${encodeURIComponent(selectedAgentId)}/${tab}`
252
+ : `/agents/${encodeURIComponent(selectedAgentId)}`;
253
+ setLocation(safePath);
254
+ }}
237
255
  onNavigateToBrowseFile=${browseActions.navigateToBrowseFile}
238
256
  onSetLocation=${setLocation}
239
257
  />
240
258
  </div>
259
+ <div
260
+ class="app-content-pane cron-pane"
261
+ style=${{ display: isCronRoute ? "block" : "none" }}
262
+ >
263
+ <${CronRoute}
264
+ jobId=${selectedCronJobId}
265
+ onSetLocation=${setLocation}
266
+ />
267
+ </div>
241
268
  <div
242
269
  class="app-content-pane"
243
270
  onscroll=${shellActions.handlePaneScroll}
244
- style=${{ display: browseState.isBrowseRoute || isAgentsRoute ? "none" : "block" }}
271
+ style=${{ display: browseState.isBrowseRoute || isAgentsRoute || isCronRoute ? "none" : "block" }}
245
272
  >
246
273
  <div
247
274
  class=${`mobile-topbar ${shellState.mobileTopbarScrolled ? "is-scrolled" : ""}`}
@@ -269,7 +296,7 @@ const App = () => {
269
296
  </span>
270
297
  </div>
271
298
  <div class="max-w-2xl w-full mx-auto">
272
- ${!browseState.isBrowseRoute && !isAgentsRoute
299
+ ${!browseState.isBrowseRoute && !isAgentsRoute && !isCronRoute
273
300
  ? html`
274
301
  <${Switch}>
275
302
  <${Route} path="/general">
@@ -408,11 +435,19 @@ const App = () => {
408
435
  `;
409
436
  };
410
437
 
411
- render(
412
- html`
413
- <${Router} hook=${useHashLocation}>
414
- <${App} />
415
- </${Router}>
416
- `,
417
- document.getElementById("app"),
418
- );
438
+ const rootElement = document.getElementById("app");
439
+ if (rootElement) {
440
+ const appBootCounter = "__alphaclawSetupAppBootCount";
441
+ window[appBootCounter] = Number(window[appBootCounter] || 0) + 1;
442
+ // Defensive: clear root so duplicate bootstraps cannot stack full app shells.
443
+ render(null, rootElement);
444
+ rootElement.replaceChildren();
445
+ render(
446
+ html`
447
+ <${Router} hook=${useHashLocation}>
448
+ <${App} />
449
+ </${Router}>
450
+ `,
451
+ rootElement,
452
+ );
453
+ }
@@ -72,19 +72,21 @@ export const ActionButton = ({
72
72
  : "opacity-80"
73
73
  }`
74
74
  : "";
75
- const spinnerSizeClass = size === "md" || size === "lg" ? "h-4 w-4" : "h-3 w-3";
75
+ const spinnerSizeClass =
76
+ size === "md" || size === "lg" ? "h-4 w-4" : "h-3 w-3";
76
77
  const isInlineLoading = loadingMode === "inline";
77
78
  const IdleIcon = idleIcon;
78
- const idleContent = iconOnly && IdleIcon
79
- ? html`<${IdleIcon} className=${idleIconClassName} />`
80
- : IdleIcon
81
- ? html`
82
- <span class="inline-flex items-center gap-1.5 leading-none">
83
- <${IdleIcon} className=${idleIconClassName} />
84
- ${idleLabel}
85
- </span>
86
- `
87
- : idleLabel;
79
+ const idleContent =
80
+ iconOnly && IdleIcon
81
+ ? html`<${IdleIcon} className=${idleIconClassName} />`
82
+ : IdleIcon
83
+ ? html`
84
+ <span class="inline-flex items-center gap-1.5 leading-none">
85
+ <${IdleIcon} className=${idleIconClassName} />
86
+ ${idleLabel}
87
+ </span>
88
+ `
89
+ : idleLabel;
88
90
  const currentLabel = loading && !isInlineLoading ? loadingLabel : idleContent;
89
91
 
90
92
  return html`
@@ -98,11 +100,15 @@ export const ActionButton = ({
98
100
  >
99
101
  ${isInlineLoading
100
102
  ? html`
101
- <span class="relative inline-flex items-center justify-center leading-none">
103
+ <span
104
+ class="relative inline-flex items-center justify-center leading-none"
105
+ >
102
106
  <span class=${loading ? "invisible" : ""}>${currentLabel}</span>
103
107
  ${loading
104
108
  ? html`
105
- <span class="absolute inset-0 inline-flex items-center justify-center">
109
+ <span
110
+ class="absolute inset-0 inline-flex items-center justify-center"
111
+ >
106
112
  <${LoadingSpinner} className=${spinnerSizeClass} />
107
113
  </span>
108
114
  `
@@ -110,13 +116,13 @@ export const ActionButton = ({
110
116
  </span>
111
117
  `
112
118
  : loading
113
- ? html`
114
- <span class="inline-flex items-center gap-1.5 leading-none">
115
- <${LoadingSpinner} className=${spinnerSizeClass} />
116
- ${currentLabel}
117
- </span>
118
- `
119
- : currentLabel}
119
+ ? html`
120
+ <span class="inline-flex items-center gap-1.5 leading-none">
121
+ <${LoadingSpinner} className=${spinnerSizeClass} />
122
+ ${currentLabel}
123
+ </span>
124
+ `
125
+ : currentLabel}
120
126
  </button>
121
127
  `;
122
128
  };
@@ -1,22 +1,69 @@
1
1
  import { h } from "https://esm.sh/preact";
2
+ import { useState, useCallback } from "https://esm.sh/preact/hooks";
2
3
  import htm from "https://esm.sh/htm";
3
4
  import { ActionButton } from "../action-button.js";
4
5
  import { Badge } from "../badge.js";
6
+ import { PillTabs } from "../pill-tabs.js";
7
+ import { PopActions } from "../pop-actions.js";
5
8
  import { AgentOverview } from "./agent-overview/index.js";
9
+ import { AgentToolsPanel } from "./agent-tools/index.js";
10
+ import { useAgentTools } from "./agent-tools/use-agent-tools.js";
6
11
 
7
12
  const html = htm.bind(h);
8
13
 
14
+ const kDetailTabs = [
15
+ { label: "Overview", value: "overview" },
16
+ { label: "Tools", value: "tools" },
17
+ ];
18
+
19
+ const PencilIcon = ({ className = "w-3.5 h-3.5" }) => html`
20
+ <svg
21
+ xmlns="http://www.w3.org/2000/svg"
22
+ viewBox="0 0 24 24"
23
+ fill="currentColor"
24
+ class=${className}
25
+ >
26
+ <path
27
+ d="M15.7279 9.57627L14.3137 8.16206L5 17.4758V18.89H6.41421L15.7279 9.57627ZM17.1421 8.16206L18.5563 6.74785L17.1421 5.33363L15.7279 6.74785L17.1421 8.16206ZM7.24264 20.89H3V16.6473L16.435 3.21231C16.8256 2.82179 17.4587 2.82179 17.8492 3.21231L20.6777 6.04074C21.0682 6.43126 21.0682 7.06443 20.6777 7.45495L7.24264 20.89Z"
28
+ />
29
+ </svg>
30
+ `;
31
+
9
32
  export const AgentDetailPanel = ({
10
33
  agent = null,
11
34
  agents = [],
35
+ activeTab = "overview",
12
36
  saving = false,
13
37
  onUpdateAgent = async () => {},
14
38
  onSetLocation = () => {},
39
+ onSelectTab = () => {},
15
40
  onEdit = () => {},
16
41
  onDelete = () => {},
17
42
  onSetDefault = () => {},
18
43
  onOpenWorkspace = () => {},
19
44
  }) => {
45
+ const tools = useAgentTools({ agent: agent || {} });
46
+ const [savingTools, setSavingTools] = useState(false);
47
+
48
+ const handleSaveTools = useCallback(async () => {
49
+ if (!agent) return;
50
+ setSavingTools(true);
51
+ try {
52
+ const nextAgent = await onUpdateAgent(
53
+ agent.id,
54
+ { tools: tools.toolsConfig },
55
+ "Tool access updated",
56
+ );
57
+ tools.markSaved(nextAgent?.tools || tools.toolsConfig);
58
+ } catch {
59
+ // toast handled by parent
60
+ } finally {
61
+ setSavingTools(false);
62
+ }
63
+ }, [agent, tools.toolsConfig, tools.markSaved, onUpdateAgent]);
64
+
65
+ const isSaving = saving || savingTools;
66
+
20
67
  if (!agent) {
21
68
  return html`
22
69
  <div class="agents-detail-panel">
@@ -32,10 +79,18 @@ export const AgentDetailPanel = ({
32
79
  <div class="agents-detail-inner">
33
80
  <div class="agents-detail-header">
34
81
  <div class="min-w-0">
35
- <div class="flex items-center gap-3 min-w-0">
82
+ <div class="flex items-center gap-2 min-w-0">
36
83
  <span class="agents-detail-header-title">
37
84
  ${agent.name || agent.id}
38
85
  </span>
86
+ <button
87
+ type="button"
88
+ class="text-gray-500 hover:text-gray-300 transition-colors p-0.5 -ml-0.5"
89
+ onclick=${() => onEdit(agent)}
90
+ title="Edit agent name"
91
+ >
92
+ <${PencilIcon} />
93
+ </button>
39
94
  ${agent.default
40
95
  ? html`<${Badge} tone="cyan">Default</${Badge}>`
41
96
  : null}
@@ -44,29 +99,55 @@ export const AgentDetailPanel = ({
44
99
  <span class="font-mono">${agent.id}</span>
45
100
  </div>
46
101
  </div>
47
- <div class="flex items-start gap-2 shrink-0">
102
+ <${PopActions} visible=${tools.dirty}>
48
103
  <${ActionButton}
49
- onClick=${() => onEdit(agent)}
50
- disabled=${saving}
104
+ onClick=${tools.reset}
105
+ disabled=${isSaving}
51
106
  tone="secondary"
52
107
  size="sm"
53
- idleLabel="Edit"
108
+ idleLabel="Cancel"
54
109
  className="text-xs"
55
110
  />
56
- </div>
111
+ <${ActionButton}
112
+ onClick=${handleSaveTools}
113
+ disabled=${isSaving}
114
+ loading=${isSaving}
115
+ loadingMode="inline"
116
+ tone="primary"
117
+ size="sm"
118
+ idleLabel="Save changes"
119
+ loadingLabel="Saving…"
120
+ className="text-xs"
121
+ />
122
+ </${PopActions}>
57
123
  </div>
124
+ <${PillTabs}
125
+ tabs=${kDetailTabs}
126
+ activeTab=${activeTab}
127
+ onSelectTab=${onSelectTab}
128
+ className="flex items-center gap-2 pt-6"
129
+ />
58
130
  <div class="agents-detail-content">
59
- <${AgentOverview}
60
- agent=${agent}
61
- agents=${agents}
62
- saving=${saving}
63
- onUpdateAgent=${onUpdateAgent}
64
- onSetLocation=${onSetLocation}
65
- onOpenWorkspace=${onOpenWorkspace}
66
- onSwitchToModels=${() => onSetLocation("/models")}
67
- onSetDefault=${onSetDefault}
68
- onDelete=${onDelete}
69
- />
131
+ ${activeTab === "overview"
132
+ ? html`
133
+ <${AgentOverview}
134
+ agent=${agent}
135
+ agents=${agents}
136
+ saving=${saving}
137
+ onUpdateAgent=${onUpdateAgent}
138
+ onSetLocation=${onSetLocation}
139
+ onOpenWorkspace=${onOpenWorkspace}
140
+ onSwitchToModels=${() => onSetLocation("/models")}
141
+ onSetDefault=${onSetDefault}
142
+ onDelete=${onDelete}
143
+ />
144
+ `
145
+ : html`
146
+ <${AgentToolsPanel}
147
+ agent=${agent}
148
+ tools=${tools}
149
+ />
150
+ `}
70
151
  </div>
71
152
  </div>
72
153
  </div>
@@ -0,0 +1,105 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+ import { ToggleSwitch } from "../../toggle-switch.js";
4
+ import { InfoTooltip } from "../../info-tooltip.js";
5
+ import { SegmentedControl } from "../../segmented-control.js";
6
+ import { kSections, kToolProfiles, kProfileLabels } from "./tool-catalog.js";
7
+
8
+ const html = htm.bind(h);
9
+
10
+ const kProfileDescriptions = {
11
+ minimal: "Only session status — grant specific tools with alsoAllow",
12
+ messaging: "Session access and messaging — ideal for notification agents",
13
+ coding: "File I/O, shell, memory, sessions, cron, and image generation",
14
+ full: "All tools enabled, no restrictions",
15
+ };
16
+
17
+ const kProfileOptions = kToolProfiles.map((p) => ({
18
+ label: kProfileLabels[p],
19
+ value: p,
20
+ title: kProfileDescriptions[p],
21
+ }));
22
+
23
+ const ToolRow = ({ tool, onToggle }) => html`
24
+ <div class="flex items-center justify-between gap-3 py-2.5 px-4">
25
+ <div class="min-w-0">
26
+ <div class="text-sm text-gray-200 flex items-center gap-1.5">
27
+ <span>${tool.label}</span>
28
+ ${tool.help
29
+ ? html`<${InfoTooltip} text=${tool.help} widthClass="w-72" />`
30
+ : null}
31
+ </div>
32
+ <span class="text-xs font-mono text-gray-500">${tool.id}</span>
33
+ </div>
34
+ <${ToggleSwitch}
35
+ checked=${tool.enabled}
36
+ onChange=${(checked) => onToggle(tool.id, checked)}
37
+ label=${null}
38
+ />
39
+ </div>
40
+ `;
41
+
42
+ const ToolSection = ({ section, toolStates, onToggle }) => {
43
+ const sectionTools = toolStates.filter((t) => t.section === section.id);
44
+ if (!sectionTools.length) return null;
45
+
46
+ return html`
47
+ <div class="bg-surface border border-border rounded-xl overflow-hidden">
48
+ <h3 class="card-label text-xs px-4 pt-3 pb-2">${section.label}</h3>
49
+ <div class="divide-y divide-border">
50
+ ${sectionTools.map(
51
+ (tool) =>
52
+ html`<${ToolRow}
53
+ key=${tool.id}
54
+ tool=${tool}
55
+ onToggle=${onToggle}
56
+ />`,
57
+ )}
58
+ </div>
59
+ </div>
60
+ `;
61
+ };
62
+
63
+ export const AgentToolsPanel = ({ agent = {}, tools = {} }) => {
64
+ const { profile, toolStates, setProfile, toggleTool } = tools;
65
+
66
+ const enabledTotal = (toolStates || []).filter((t) => t.enabled).length;
67
+ const totalTools = (toolStates || []).length;
68
+
69
+ return html`
70
+ <div class="space-y-4">
71
+ <div class="bg-surface border border-border rounded-xl p-4 space-y-4">
72
+ <div>
73
+ <div class="flex items-center justify-between mb-3">
74
+ <h3 class="card-label text-xs">Preset</h3>
75
+ <span class="text-xs text-gray-500"
76
+ >${enabledTotal}/${totalTools} tools enabled</span
77
+ >
78
+ </div>
79
+ <${SegmentedControl}
80
+ options=${kProfileOptions}
81
+ value=${profile}
82
+ onChange=${setProfile}
83
+ fullWidth
84
+ className="ac-segmented-control-dark"
85
+ />
86
+ </div>
87
+ </div>
88
+
89
+ <div style="columns: 2; column-gap: 0.75rem;">
90
+ ${kSections.map(
91
+ (section) => html`
92
+ <div style="break-inside: avoid; margin-bottom: 0.75rem;">
93
+ <${ToolSection}
94
+ key=${section.id}
95
+ section=${section}
96
+ toolStates=${toolStates || []}
97
+ onToggle=${toggleTool}
98
+ />
99
+ </div>
100
+ `,
101
+ )}
102
+ </div>
103
+ </div>
104
+ `;
105
+ };