@chrysb/alphaclaw 0.3.3 → 0.3.4

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 (31) hide show
  1. package/bin/alphaclaw.js +18 -0
  2. package/lib/plugin/usage-tracker/index.js +308 -0
  3. package/lib/plugin/usage-tracker/openclaw.plugin.json +8 -0
  4. package/lib/public/css/explorer.css +51 -1
  5. package/lib/public/css/shell.css +3 -1
  6. package/lib/public/css/theme.css +35 -0
  7. package/lib/public/js/app.js +73 -24
  8. package/lib/public/js/components/file-tree.js +231 -28
  9. package/lib/public/js/components/file-viewer.js +193 -20
  10. package/lib/public/js/components/segmented-control.js +33 -0
  11. package/lib/public/js/components/sidebar.js +14 -32
  12. package/lib/public/js/components/telegram-workspace/index.js +353 -0
  13. package/lib/public/js/components/telegram-workspace/manage.js +397 -0
  14. package/lib/public/js/components/telegram-workspace/onboarding.js +616 -0
  15. package/lib/public/js/components/usage-tab.js +528 -0
  16. package/lib/public/js/components/watchdog-tab.js +1 -1
  17. package/lib/public/js/lib/api.js +25 -1
  18. package/lib/public/js/lib/telegram-api.js +78 -0
  19. package/lib/public/js/lib/ui-settings.js +38 -0
  20. package/lib/public/setup.html +34 -30
  21. package/lib/server/alphaclaw-version.js +3 -3
  22. package/lib/server/constants.js +1 -0
  23. package/lib/server/onboarding/openclaw.js +15 -0
  24. package/lib/server/routes/auth.js +5 -1
  25. package/lib/server/routes/telegram.js +185 -60
  26. package/lib/server/routes/usage.js +133 -0
  27. package/lib/server/usage-db.js +570 -0
  28. package/lib/server.js +21 -1
  29. package/lib/setup/core-prompts/AGENTS.md +0 -101
  30. package/package.json +1 -1
  31. package/lib/public/js/components/telegram-workspace.js +0 -1365
@@ -1,5 +1,11 @@
1
1
  import { h } from "https://esm.sh/preact";
2
- import { useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "https://esm.sh/preact/hooks";
3
9
  import htm from "https://esm.sh/htm";
4
10
  import { fetchBrowseTree } from "../lib/api.js";
5
11
  import {
@@ -23,6 +29,7 @@ const html = htm.bind(h);
23
29
  const kTreeIndentPx = 9;
24
30
  const kFolderBasePaddingPx = 10;
25
31
  const kFileBasePaddingPx = 14;
32
+ const kTreeRefreshIntervalMs = 5000;
26
33
  const kCollapsedFoldersStorageKey = "alphaclaw.browse.collapsedFolders";
27
34
  const kLegacyCollapsedFoldersStorageKey = "alphaclawBrowseCollapsedFolders";
28
35
 
@@ -48,6 +55,37 @@ const collectFolderPaths = (node, folderPaths) => {
48
55
  );
49
56
  };
50
57
 
58
+ const collectFilePaths = (node, filePaths) => {
59
+ if (!node) return;
60
+ if (node.type === "file") {
61
+ if (node.path) filePaths.push(node.path);
62
+ return;
63
+ }
64
+ (node.children || []).forEach((childNode) =>
65
+ collectFilePaths(childNode, filePaths),
66
+ );
67
+ };
68
+
69
+ const filterTreeNode = (node, normalizedQuery) => {
70
+ if (!node) return null;
71
+ const query = String(normalizedQuery || "").trim().toLowerCase();
72
+ if (!query) return node;
73
+ const nodeName = String(node.name || "").toLowerCase();
74
+ const nodePath = String(node.path || "").toLowerCase();
75
+ const isDirectMatch = nodeName.includes(query) || nodePath.includes(query);
76
+ if (node.type === "file") {
77
+ return isDirectMatch ? node : null;
78
+ }
79
+ const filteredChildren = (node.children || [])
80
+ .map((childNode) => filterTreeNode(childNode, query))
81
+ .filter(Boolean);
82
+ if (!isDirectMatch && filteredChildren.length === 0) return null;
83
+ return {
84
+ ...node,
85
+ children: filteredChildren,
86
+ };
87
+ };
88
+
51
89
  const getFileIconMeta = (fileName) => {
52
90
  const normalizedName = String(fileName || "").toLowerCase();
53
91
  if (normalizedName.endsWith(".md")) {
@@ -126,17 +164,20 @@ const TreeNode = ({
126
164
  onSelectFile,
127
165
  selectedPath = "",
128
166
  draftPaths,
167
+ isSearchActive = false,
168
+ searchActivePath = "",
129
169
  }) => {
130
170
  if (!node) return null;
131
171
  if (node.type === "file") {
132
172
  const isActive = selectedPath === node.path;
173
+ const isSearchActiveNode = searchActivePath === node.path;
133
174
  const hasDraft = draftPaths.has(node.path || "");
134
175
  const fileIconMeta = getFileIconMeta(node.name);
135
176
  const FileTypeIcon = fileIconMeta.icon;
136
177
  return html`
137
178
  <li class="tree-item">
138
179
  <a
139
- class=${isActive ? "active" : ""}
180
+ class=${`${isActive ? "active" : ""} ${isSearchActiveNode && !isActive ? "soft-active" : ""}`.trim()}
140
181
  onclick=${() => onSelectFile(node.path)}
141
182
  style=${{
142
183
  paddingLeft: `${kFileBasePaddingPx + depth * kTreeIndentPx}px`,
@@ -152,7 +193,7 @@ const TreeNode = ({
152
193
  }
153
194
 
154
195
  const folderPath = node.path || "";
155
- const isCollapsed = collapsedPaths.has(folderPath);
196
+ const isCollapsed = isSearchActive ? false : collapsedPaths.has(folderPath);
156
197
  return html`
157
198
  <li class="tree-item">
158
199
  <div
@@ -178,6 +219,8 @@ const TreeNode = ({
178
219
  onSelectFile=${onSelectFile}
179
220
  selectedPath=${selectedPath}
180
221
  draftPaths=${draftPaths}
222
+ isSearchActive=${isSearchActive}
223
+ searchActivePath=${searchActivePath}
181
224
  />
182
225
  `,
183
226
  )}
@@ -186,7 +229,11 @@ const TreeNode = ({
186
229
  `;
187
230
  };
188
231
 
189
- export const FileTree = ({ onSelectFile = () => {}, selectedPath = "" }) => {
232
+ export const FileTree = ({
233
+ onSelectFile = () => {},
234
+ selectedPath = "",
235
+ onPreviewFile = () => {},
236
+ }) => {
190
237
  const [treeRoot, setTreeRoot] = useState(null);
191
238
  const [loading, setLoading] = useState(true);
192
239
  const [error, setError] = useState("");
@@ -194,38 +241,75 @@ export const FileTree = ({ onSelectFile = () => {}, selectedPath = "" }) => {
194
241
  readStoredCollapsedPaths,
195
242
  );
196
243
  const [draftPaths, setDraftPaths] = useState(readStoredDraftPaths);
244
+ const [searchQuery, setSearchQuery] = useState("");
245
+ const [searchActivePath, setSearchActivePath] = useState("");
246
+ const searchInputRef = useRef(null);
247
+ const treeSignatureRef = useRef("");
197
248
 
198
- useEffect(() => {
199
- let active = true;
200
- const loadTree = async () => {
201
- setLoading(true);
202
- setError("");
203
- try {
204
- const data = await fetchBrowseTree();
205
- if (!active) return;
206
- setTreeRoot(data.root || null);
207
- setCollapsedPaths((previousPaths) => {
208
- if (previousPaths instanceof Set) return previousPaths;
209
- const nextPaths = new Set();
210
- collectFolderPaths(data.root, nextPaths);
211
- return nextPaths;
212
- });
213
- } catch (loadError) {
214
- if (!active) return;
249
+ const loadTree = useCallback(async ({ showLoading = false } = {}) => {
250
+ if (showLoading) setLoading(true);
251
+ if (showLoading) setError("");
252
+ try {
253
+ const data = await fetchBrowseTree();
254
+ const nextRoot = data.root || null;
255
+ const nextSignature = JSON.stringify(nextRoot || {});
256
+ if (treeSignatureRef.current !== nextSignature) {
257
+ treeSignatureRef.current = nextSignature;
258
+ setTreeRoot(nextRoot);
259
+ }
260
+ setCollapsedPaths((previousPaths) => {
261
+ if (previousPaths instanceof Set) return previousPaths;
262
+ const nextPaths = new Set();
263
+ collectFolderPaths(nextRoot, nextPaths);
264
+ return nextPaths;
265
+ });
266
+ if (showLoading) setError("");
267
+ } catch (loadError) {
268
+ if (showLoading) {
215
269
  setError(loadError.message || "Could not load file tree");
216
- } finally {
217
- if (active) setLoading(false);
218
270
  }
271
+ } finally {
272
+ if (showLoading) setLoading(false);
273
+ }
274
+ }, []);
275
+
276
+ useEffect(() => {
277
+ loadTree({ showLoading: true });
278
+ }, [loadTree]);
279
+
280
+ useEffect(() => {
281
+ const refreshTree = () => {
282
+ loadTree({ showLoading: false });
219
283
  };
220
- loadTree();
284
+ const refreshInterval = window.setInterval(
285
+ refreshTree,
286
+ kTreeRefreshIntervalMs,
287
+ );
288
+ window.addEventListener("alphaclaw:browse-file-saved", refreshTree);
289
+ window.addEventListener("alphaclaw:browse-tree-refresh", refreshTree);
221
290
  return () => {
222
- active = false;
291
+ window.clearInterval(refreshInterval);
292
+ window.removeEventListener("alphaclaw:browse-file-saved", refreshTree);
293
+ window.removeEventListener("alphaclaw:browse-tree-refresh", refreshTree);
223
294
  };
224
- }, []);
295
+ }, [loadTree]);
225
296
 
226
- const rootChildren = useMemo(() => treeRoot?.children || [], [treeRoot]);
297
+ const normalizedSearchQuery = String(searchQuery || "").trim().toLowerCase();
298
+ const rootChildren = useMemo(() => {
299
+ const children = treeRoot?.children || [];
300
+ if (!normalizedSearchQuery) return children;
301
+ return children
302
+ .map((node) => filterTreeNode(node, normalizedSearchQuery))
303
+ .filter(Boolean);
304
+ }, [treeRoot, normalizedSearchQuery]);
227
305
  const safeCollapsedPaths =
228
306
  collapsedPaths instanceof Set ? collapsedPaths : new Set();
307
+ const isSearchActive = normalizedSearchQuery.length > 0;
308
+ const filteredFilePaths = useMemo(() => {
309
+ const filePaths = [];
310
+ rootChildren.forEach((node) => collectFilePaths(node, filePaths));
311
+ return filePaths;
312
+ }, [rootChildren]);
229
313
 
230
314
  useEffect(() => {
231
315
  if (!(collapsedPaths instanceof Set)) return;
@@ -278,6 +362,39 @@ export const FileTree = ({ onSelectFile = () => {}, selectedPath = "" }) => {
278
362
  };
279
363
  }, []);
280
364
 
365
+ useEffect(() => {
366
+ const handleGlobalSearchShortcut = (event) => {
367
+ if (event.key !== "/") return;
368
+ if (event.metaKey || event.ctrlKey || event.altKey) return;
369
+ const target = event.target;
370
+ const tagName = String(target?.tagName || "").toLowerCase();
371
+ const isTypingTarget =
372
+ tagName === "input" ||
373
+ tagName === "textarea" ||
374
+ tagName === "select" ||
375
+ target?.isContentEditable;
376
+ if (isTypingTarget && target !== searchInputRef.current) return;
377
+ event.preventDefault();
378
+ searchInputRef.current?.focus();
379
+ searchInputRef.current?.select();
380
+ };
381
+ window.addEventListener("keydown", handleGlobalSearchShortcut);
382
+ return () => {
383
+ window.removeEventListener("keydown", handleGlobalSearchShortcut);
384
+ };
385
+ }, []);
386
+
387
+ useEffect(() => {
388
+ if (!isSearchActive) {
389
+ setSearchActivePath("");
390
+ onPreviewFile("");
391
+ return;
392
+ }
393
+ if (searchActivePath && filteredFilePaths.includes(searchActivePath)) return;
394
+ setSearchActivePath("");
395
+ onPreviewFile("");
396
+ }, [isSearchActive, filteredFilePaths, searchActivePath, onPreviewFile]);
397
+
281
398
  const toggleFolder = (folderPath) => {
282
399
  setCollapsedPaths((previousPaths) => {
283
400
  const nextPaths =
@@ -288,6 +405,58 @@ export const FileTree = ({ onSelectFile = () => {}, selectedPath = "" }) => {
288
405
  });
289
406
  };
290
407
 
408
+ const updateSearchQuery = (nextQuery) => {
409
+ setSearchQuery(nextQuery);
410
+ };
411
+
412
+ const clearSearch = () => {
413
+ setSearchQuery("");
414
+ setSearchActivePath("");
415
+ onPreviewFile("");
416
+ };
417
+
418
+ const moveSearchSelection = (direction) => {
419
+ if (!filteredFilePaths.length) return;
420
+ const currentIndex = filteredFilePaths.indexOf(searchActivePath);
421
+ const delta = direction === "up" ? -1 : 1;
422
+ const baseIndex = currentIndex === -1 ? (direction === "up" ? 0 : -1) : currentIndex;
423
+ const nextIndex =
424
+ (baseIndex + delta + filteredFilePaths.length) % filteredFilePaths.length;
425
+ const nextPath = filteredFilePaths[nextIndex];
426
+ setSearchActivePath(nextPath);
427
+ onPreviewFile(nextPath);
428
+ };
429
+
430
+ const commitSearchSelection = () => {
431
+ const [singlePath = ""] = filteredFilePaths;
432
+ const targetPath = searchActivePath || (filteredFilePaths.length === 1 ? singlePath : "");
433
+ if (!targetPath) return;
434
+ onSelectFile(targetPath);
435
+ clearSearch();
436
+ };
437
+
438
+ const onSearchKeyDown = (event) => {
439
+ if (event.key === "ArrowDown") {
440
+ event.preventDefault();
441
+ moveSearchSelection("down");
442
+ return;
443
+ }
444
+ if (event.key === "ArrowUp") {
445
+ event.preventDefault();
446
+ moveSearchSelection("up");
447
+ return;
448
+ }
449
+ if (event.key === "Enter") {
450
+ event.preventDefault();
451
+ commitSearchSelection();
452
+ return;
453
+ }
454
+ if (event.key === "Escape") {
455
+ event.preventDefault();
456
+ clearSearch();
457
+ }
458
+ };
459
+
291
460
  if (loading) {
292
461
  return html`<div class="file-tree-state">Loading files...</div>`;
293
462
  }
@@ -297,11 +466,43 @@ export const FileTree = ({ onSelectFile = () => {}, selectedPath = "" }) => {
297
466
  </div>`;
298
467
  }
299
468
  if (!rootChildren.length) {
300
- return html`<div class="file-tree-state">No files found.</div>`;
469
+ return html`
470
+ <div class="file-tree-wrap">
471
+ <div class="file-tree-search">
472
+ <input
473
+ class="file-tree-search-input"
474
+ type="text"
475
+ ref=${searchInputRef}
476
+ value=${searchQuery}
477
+ onInput=${(event) => updateSearchQuery(event.target.value)}
478
+ onKeyDown=${onSearchKeyDown}
479
+ placeholder="Search files..."
480
+ autocomplete="off"
481
+ spellcheck=${false}
482
+ />
483
+ </div>
484
+ <div class="file-tree-state">
485
+ ${isSearchActive ? "No matching files." : "No files found."}
486
+ </div>
487
+ </div>
488
+ `;
301
489
  }
302
490
 
303
491
  return html`
304
492
  <div class="file-tree-wrap">
493
+ <div class="file-tree-search">
494
+ <input
495
+ class="file-tree-search-input"
496
+ type="text"
497
+ ref=${searchInputRef}
498
+ value=${searchQuery}
499
+ onInput=${(event) => updateSearchQuery(event.target.value)}
500
+ onKeyDown=${onSearchKeyDown}
501
+ placeholder="Search files..."
502
+ autocomplete="off"
503
+ spellcheck=${false}
504
+ />
505
+ </div>
305
506
  <ul class="file-tree">
306
507
  ${rootChildren.map(
307
508
  (node) => html`
@@ -313,6 +514,8 @@ export const FileTree = ({ onSelectFile = () => {}, selectedPath = "" }) => {
313
514
  onSelectFile=${onSelectFile}
314
515
  selectedPath=${selectedPath}
315
516
  draftPaths=${draftPaths}
517
+ isSearchActive=${isSearchActive}
518
+ searchActivePath=${searchActivePath}
316
519
  />
317
520
  `,
318
521
  )}