@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
@@ -22,13 +22,18 @@ import {
22
22
  writeStoredFileDraft,
23
23
  } from "../lib/browse-draft-state.js";
24
24
  import { ActionButton } from "./action-button.js";
25
+ import { LoadingSpinner } from "./loading-spinner.js";
26
+ import { SegmentedControl } from "./segmented-control.js";
25
27
  import { SaveFillIcon } from "./icons.js";
26
28
  import { showToast } from "./toast.js";
27
29
 
28
30
  const html = htm.bind(h);
29
31
  const kFileViewerModeStorageKey = "alphaclaw.browse.fileViewerMode";
30
32
  const kLegacyFileViewerModeStorageKey = "alphaclawBrowseFileViewerMode";
33
+ const kEditorSelectionStorageKey = "alphaclaw.browse.editorSelectionByPath";
31
34
  const kProtectedBrowsePaths = new Set(["openclaw.json", "devices/paired.json"]);
35
+ const kLoadingIndicatorDelayMs = 1000;
36
+ const kFileRefreshIntervalMs = 5000;
32
37
 
33
38
  const parsePathSegments = (inputPath) =>
34
39
  String(inputPath || "")
@@ -57,18 +62,70 @@ const readStoredFileViewerMode = () => {
57
62
  }
58
63
  };
59
64
 
65
+ const clampSelectionIndex = (value, maxValue) => {
66
+ const numericValue = Number.parseInt(String(value ?? ""), 10);
67
+ if (!Number.isFinite(numericValue)) return 0;
68
+ return Math.max(0, Math.min(maxValue, numericValue));
69
+ };
70
+
71
+ const readStoredEditorSelection = (filePath) => {
72
+ const safePath = String(filePath || "").trim();
73
+ if (!safePath) return null;
74
+ try {
75
+ const rawStorageValue = window.localStorage.getItem(kEditorSelectionStorageKey);
76
+ if (!rawStorageValue) return null;
77
+ const parsedStorageValue = JSON.parse(rawStorageValue);
78
+ if (!parsedStorageValue || typeof parsedStorageValue !== "object") return null;
79
+ const selection = parsedStorageValue[safePath];
80
+ if (!selection || typeof selection !== "object") return null;
81
+ return {
82
+ start: selection.start,
83
+ end: selection.end,
84
+ };
85
+ } catch {
86
+ return null;
87
+ }
88
+ };
60
89
 
61
- export const FileViewer = ({ filePath = "" }) => {
90
+ const writeStoredEditorSelection = (filePath, selection) => {
91
+ const safePath = String(filePath || "").trim();
92
+ if (!safePath || !selection || typeof selection !== "object") return;
93
+ try {
94
+ const rawStorageValue = window.localStorage.getItem(kEditorSelectionStorageKey);
95
+ const parsedStorageValue = rawStorageValue ? JSON.parse(rawStorageValue) : {};
96
+ const nextStorageValue =
97
+ parsedStorageValue && typeof parsedStorageValue === "object"
98
+ ? parsedStorageValue
99
+ : {};
100
+ nextStorageValue[safePath] = {
101
+ start: selection.start,
102
+ end: selection.end,
103
+ };
104
+ window.localStorage.setItem(
105
+ kEditorSelectionStorageKey,
106
+ JSON.stringify(nextStorageValue),
107
+ );
108
+ } catch {}
109
+ };
110
+
111
+
112
+ export const FileViewer = ({
113
+ filePath = "",
114
+ isPreviewOnly = false,
115
+ }) => {
62
116
  const normalizedPath = String(filePath || "").trim();
63
117
  const normalizedPolicyPath = normalizePolicyPath(normalizedPath);
64
118
  const [content, setContent] = useState("");
65
119
  const [initialContent, setInitialContent] = useState("");
66
120
  const [viewMode, setViewMode] = useState(readStoredFileViewerMode);
67
121
  const [loading, setLoading] = useState(false);
122
+ const [showDelayedLoadingSpinner, setShowDelayedLoadingSpinner] =
123
+ useState(false);
68
124
  const [saving, setSaving] = useState(false);
69
125
  const [error, setError] = useState("");
70
126
  const [isFolderPath, setIsFolderPath] = useState(false);
71
127
  const [frontmatterCollapsed, setFrontmatterCollapsed] = useState(false);
128
+ const [externalChangeNoticeShown, setExternalChangeNoticeShown] = useState(false);
72
129
  const [protectedEditBypassPaths, setProtectedEditBypassPaths] = useState(
73
130
  () => new Set(),
74
131
  );
@@ -79,6 +136,8 @@ export const FileViewer = ({ filePath = "" }) => {
79
136
  const viewScrollRatioRef = useRef(0);
80
137
  const isSyncingScrollRef = useRef(false);
81
138
  const loadedFilePathRef = useRef("");
139
+ const restoredSelectionPathRef = useRef("");
140
+ const fileRefreshInFlightRef = useRef(false);
82
141
  const editorLineNumberRowRefs = useRef([]);
83
142
  const editorHighlightLineRefs = useRef([]);
84
143
 
@@ -87,7 +146,7 @@ export const FileViewer = ({ filePath = "" }) => {
87
146
  [normalizedPath],
88
147
  );
89
148
  const hasSelectedPath = normalizedPath.length > 0;
90
- const canEditFile = hasSelectedPath && !isFolderPath;
149
+ const canEditFile = hasSelectedPath && !isFolderPath && !isPreviewOnly;
91
150
  const isDirty = canEditFile && content !== initialContent;
92
151
  const isProtectedFile =
93
152
  canEditFile && kProtectedBrowsePaths.has(normalizedPolicyPath);
@@ -164,9 +223,21 @@ export const FileViewer = ({ filePath = "" }) => {
164
223
  } catch {}
165
224
  }, [viewMode]);
166
225
 
226
+ useEffect(() => {
227
+ if (!loading) {
228
+ setShowDelayedLoadingSpinner(false);
229
+ return () => {};
230
+ }
231
+ const timer = window.setTimeout(() => {
232
+ setShowDelayedLoadingSpinner(true);
233
+ }, kLoadingIndicatorDelayMs);
234
+ return () => window.clearTimeout(timer);
235
+ }, [loading]);
236
+
167
237
  useEffect(() => {
168
238
  let active = true;
169
239
  loadedFilePathRef.current = "";
240
+ restoredSelectionPathRef.current = "";
170
241
  if (!hasSelectedPath) {
171
242
  setContent("");
172
243
  setInitialContent("");
@@ -195,8 +266,10 @@ export const FileViewer = ({ filePath = "" }) => {
195
266
  { dispatchEvent: (event) => window.dispatchEvent(event) },
196
267
  );
197
268
  setInitialContent(nextContent);
269
+ setExternalChangeNoticeShown(false);
198
270
  viewScrollRatioRef.current = 0;
199
271
  loadedFilePathRef.current = normalizedPath;
272
+ restoredSelectionPathRef.current = "";
200
273
  } catch (loadError) {
201
274
  if (!active) return;
202
275
  const message = loadError.message || "Could not load file";
@@ -206,6 +279,7 @@ export const FileViewer = ({ filePath = "" }) => {
206
279
  setIsFolderPath(true);
207
280
  setError("");
208
281
  loadedFilePathRef.current = normalizedPath;
282
+ restoredSelectionPathRef.current = "";
209
283
  return;
210
284
  }
211
285
  setError(message);
@@ -219,6 +293,60 @@ export const FileViewer = ({ filePath = "" }) => {
219
293
  };
220
294
  }, [hasSelectedPath, normalizedPath]);
221
295
 
296
+ useEffect(() => {
297
+ if (!hasSelectedPath || isFolderPath || !canEditFile) return () => {};
298
+ const refreshFromDisk = async () => {
299
+ if (loading || saving) return;
300
+ if (fileRefreshInFlightRef.current) return;
301
+ fileRefreshInFlightRef.current = true;
302
+ try {
303
+ const data = await fetchFileContent(normalizedPath);
304
+ const diskContent = data.content || "";
305
+ if (diskContent === initialContent) {
306
+ setExternalChangeNoticeShown(false);
307
+ return;
308
+ }
309
+ // Auto-refresh only when editor has no unsaved work.
310
+ if (!isDirty) {
311
+ setContent(diskContent);
312
+ setInitialContent(diskContent);
313
+ clearStoredFileDraft(normalizedPath);
314
+ updateDraftIndex(normalizedPath, false, {
315
+ dispatchEvent: (event) => window.dispatchEvent(event),
316
+ });
317
+ setExternalChangeNoticeShown(false);
318
+ window.dispatchEvent(new CustomEvent("alphaclaw:browse-tree-refresh"));
319
+ return;
320
+ }
321
+ if (!externalChangeNoticeShown) {
322
+ showToast(
323
+ "This file changed on disk. Save to overwrite or reload by re-opening.",
324
+ "error",
325
+ );
326
+ setExternalChangeNoticeShown(true);
327
+ }
328
+ } catch {
329
+ // Ignore transient refresh errors to avoid interrupting editing.
330
+ } finally {
331
+ fileRefreshInFlightRef.current = false;
332
+ }
333
+ };
334
+ const intervalId = window.setInterval(refreshFromDisk, kFileRefreshIntervalMs);
335
+ return () => {
336
+ window.clearInterval(intervalId);
337
+ };
338
+ }, [
339
+ hasSelectedPath,
340
+ isFolderPath,
341
+ canEditFile,
342
+ loading,
343
+ saving,
344
+ normalizedPath,
345
+ initialContent,
346
+ isDirty,
347
+ externalChangeNoticeShown,
348
+ ]);
349
+
222
350
  useEffect(() => {
223
351
  if (loadedFilePathRef.current !== normalizedPath) return;
224
352
  if (!canEditFile || !hasSelectedPath || loading) return;
@@ -242,6 +370,21 @@ export const FileViewer = ({ filePath = "" }) => {
242
370
  normalizedPath,
243
371
  ]);
244
372
 
373
+ useEffect(() => {
374
+ if (!canEditFile || loading || !hasSelectedPath) return;
375
+ if (loadedFilePathRef.current !== normalizedPath) return;
376
+ if (restoredSelectionPathRef.current === normalizedPath) return;
377
+ const textareaElement = editorTextareaRef.current;
378
+ if (!textareaElement) return;
379
+ const storedSelection = readStoredEditorSelection(normalizedPath);
380
+ restoredSelectionPathRef.current = normalizedPath;
381
+ if (!storedSelection) return;
382
+ const maxIndex = String(content || "").length;
383
+ const start = clampSelectionIndex(storedSelection.start, maxIndex);
384
+ const end = clampSelectionIndex(storedSelection.end, maxIndex);
385
+ textareaElement.setSelectionRange(start, Math.max(start, end));
386
+ }, [canEditFile, loading, hasSelectedPath, normalizedPath, content]);
387
+
245
388
  const handleSave = async () => {
246
389
  if (!canEditFile || saving || !isDirty || isProtectedLocked) return;
247
390
  setSaving(true);
@@ -249,6 +392,7 @@ export const FileViewer = ({ filePath = "" }) => {
249
392
  try {
250
393
  const data = await saveFileContent(normalizedPath, content);
251
394
  setInitialContent(content);
395
+ setExternalChangeNoticeShown(false);
252
396
  clearStoredFileDraft(normalizedPath);
253
397
  updateDraftIndex(normalizedPath, false, {
254
398
  dispatchEvent: (event) => window.dispatchEvent(event),
@@ -282,9 +426,15 @@ export const FileViewer = ({ filePath = "" }) => {
282
426
  };
283
427
 
284
428
  const handleContentInput = (event) => {
285
- if (isProtectedLocked) return;
429
+ if (isProtectedLocked || isPreviewOnly) return;
286
430
  const nextContent = event.target.value;
287
431
  setContent(nextContent);
432
+ if (hasSelectedPath && canEditFile) {
433
+ writeStoredEditorSelection(normalizedPath, {
434
+ start: event.target.selectionStart,
435
+ end: event.target.selectionEnd,
436
+ });
437
+ }
288
438
  if (hasSelectedPath && canEditFile) {
289
439
  writeStoredFileDraft(normalizedPath, nextContent);
290
440
  updateDraftIndex(normalizedPath, nextContent !== initialContent, {
@@ -293,6 +443,16 @@ export const FileViewer = ({ filePath = "" }) => {
293
443
  }
294
444
  };
295
445
 
446
+ const handleEditorSelectionChange = () => {
447
+ if (!hasSelectedPath || !canEditFile || loading) return;
448
+ const textareaElement = editorTextareaRef.current;
449
+ if (!textareaElement) return;
450
+ writeStoredEditorSelection(normalizedPath, {
451
+ start: textareaElement.selectionStart,
452
+ end: textareaElement.selectionEnd,
453
+ });
454
+ };
455
+
296
456
  const getScrollRatio = (element) => {
297
457
  if (!element) return 0;
298
458
  const maxScrollTop = element.scrollHeight - element.clientHeight;
@@ -408,22 +568,20 @@ export const FileViewer = ({ filePath = "" }) => {
408
568
  : null}
409
569
  </div>
410
570
  <div class="file-viewer-tabbar-spacer"></div>
571
+ ${isPreviewOnly
572
+ ? html`<div class="file-viewer-preview-pill">Preview</div>`
573
+ : null}
411
574
  ${isMarkdownFile &&
412
575
  html`
413
- <div class="file-viewer-view-toggle">
414
- <button
415
- class=${`file-viewer-view-toggle-button ${viewMode === "edit" ? "active" : ""}`}
416
- onclick=${() => handleChangeViewMode("edit")}
417
- >
418
- edit
419
- </button>
420
- <button
421
- class=${`file-viewer-view-toggle-button ${viewMode === "preview" ? "active" : ""}`}
422
- onclick=${() => handleChangeViewMode("preview")}
423
- >
424
- preview
425
- </button>
426
- </div>
576
+ <${SegmentedControl}
577
+ className="mr-2.5"
578
+ options=${[
579
+ { label: "edit", value: "edit" },
580
+ { label: "preview", value: "preview" },
581
+ ]}
582
+ value=${viewMode}
583
+ onChange=${handleChangeViewMode}
584
+ />
427
585
  `}
428
586
  <${ActionButton}
429
587
  onClick=${handleSave}
@@ -508,7 +666,13 @@ ${formattedValue}</pre
508
666
  `
509
667
  : null}
510
668
  ${loading
511
- ? html`<div class="file-viewer-state">Loading file...</div>`
669
+ ? html`
670
+ <div class="file-viewer-loading-shell">
671
+ ${showDelayedLoadingSpinner
672
+ ? html`<${LoadingSpinner} className="h-4 w-4" />`
673
+ : null}
674
+ </div>
675
+ `
512
676
  : error
513
677
  ? html`<div class="file-viewer-state file-viewer-state-error">
514
678
  ${error}
@@ -585,6 +749,9 @@ ${formattedValue}</pre
585
749
  value=${content}
586
750
  onInput=${handleContentInput}
587
751
  onScroll=${handleEditorScroll}
752
+ onSelect=${handleEditorSelectionChange}
753
+ onKeyUp=${handleEditorSelectionChange}
754
+ onClick=${handleEditorSelectionChange}
588
755
  spellcheck=${false}
589
756
  autocorrect="off"
590
757
  autocapitalize="off"
@@ -653,7 +820,10 @@ ${formattedValue}</pre
653
820
  value=${content}
654
821
  onInput=${handleContentInput}
655
822
  onScroll=${handleEditorScroll}
656
- readonly=${isProtectedLocked}
823
+ onSelect=${handleEditorSelectionChange}
824
+ onKeyUp=${handleEditorSelectionChange}
825
+ onClick=${handleEditorSelectionChange}
826
+ readonly=${isProtectedLocked || isPreviewOnly}
657
827
  spellcheck=${false}
658
828
  autocorrect="off"
659
829
  autocapitalize="off"
@@ -672,7 +842,10 @@ ${formattedValue}</pre
672
842
  value=${content}
673
843
  onInput=${handleContentInput}
674
844
  onScroll=${handleEditorScroll}
675
- readonly=${isProtectedLocked}
845
+ onSelect=${handleEditorSelectionChange}
846
+ onKeyUp=${handleEditorSelectionChange}
847
+ onClick=${handleEditorSelectionChange}
848
+ readonly=${isProtectedLocked || isPreviewOnly}
676
849
  spellcheck=${false}
677
850
  autocorrect="off"
678
851
  autocapitalize="off"
@@ -0,0 +1,33 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import htm from "https://esm.sh/htm";
3
+
4
+ const html = htm.bind(h);
5
+
6
+ /**
7
+ * Reusable segmented control (pill toggle).
8
+ *
9
+ * @param {Object} props
10
+ * @param {Array<{label:string, value:*}>} props.options
11
+ * @param {*} props.value Currently selected value.
12
+ * @param {Function} props.onChange Called with the new value on click.
13
+ * @param {string} [props.className] Extra classes on the wrapper.
14
+ */
15
+ export const SegmentedControl = ({
16
+ options = [],
17
+ value,
18
+ onChange = () => {},
19
+ className = "",
20
+ }) => html`
21
+ <div class=${`ac-segmented-control ${className}`}>
22
+ ${options.map(
23
+ (option) => html`
24
+ <button
25
+ class=${`ac-segmented-control-button ${option.value === value ? "active" : ""}`}
26
+ onclick=${() => onChange(option.value)}
27
+ >
28
+ ${option.label}
29
+ </button>
30
+ `,
31
+ )}
32
+ </div>
33
+ `;
@@ -5,37 +5,26 @@ import { HomeLineIcon, FolderLineIcon } from "./icons.js";
5
5
  import { FileTree } from "./file-tree.js";
6
6
  import { UpdateActionButton } from "./update-action-button.js";
7
7
  import { SidebarGitPanel } from "./sidebar-git-panel.js";
8
+ import { readUiSettings, writeUiSettings } from "../lib/ui-settings.js";
8
9
 
9
10
  const html = htm.bind(h);
10
- const kUiSettingsStorageKey = "alphaclaw.uiSettings";
11
- const kLegacyUiSettingsStorageKey = "alphaclawUiSettings";
12
11
  const kBrowseBottomPanelUiSettingKey = "browseBottomPanelHeightPx";
13
12
  const kBrowsePanelMinHeightPx = 120;
14
13
  const kBrowseBottomMinHeightPx = 120;
15
14
  const kBrowseResizerHeightPx = 6;
16
15
  const kDefaultBrowseBottomPanelHeightPx = 160;
17
- const kLegacyBrowsePanelUiStorageKey = "alphaclawBrowsePanelHeightPx";
18
16
 
19
17
  const readStoredBrowseBottomPanelHeight = () => {
20
18
  try {
21
- const rawSettings =
22
- window.localStorage.getItem(kUiSettingsStorageKey) ||
23
- window.localStorage.getItem(kLegacyUiSettingsStorageKey);
24
- if (rawSettings) {
25
- const parsedSettings = JSON.parse(rawSettings);
26
- const fromSharedSettings = Number.parseInt(
27
- String(parsedSettings?.[kBrowseBottomPanelUiSettingKey] || ""),
28
- 10,
29
- );
30
- if (Number.isFinite(fromSharedSettings) && fromSharedSettings > 0) {
31
- return fromSharedSettings;
32
- }
19
+ const settings = readUiSettings();
20
+ const fromSharedSettings = Number.parseInt(
21
+ String(settings?.[kBrowseBottomPanelUiSettingKey] || ""),
22
+ 10,
23
+ );
24
+ if (Number.isFinite(fromSharedSettings) && fromSharedSettings > 0) {
25
+ return fromSharedSettings;
33
26
  }
34
- const legacyRawValue = window.localStorage.getItem(kLegacyBrowsePanelUiStorageKey);
35
- const parsedValue = Number.parseInt(String(legacyRawValue || ""), 10);
36
- return Number.isFinite(parsedValue) && parsedValue > 0
37
- ? parsedValue
38
- : kDefaultBrowseBottomPanelHeightPx;
27
+ return kDefaultBrowseBottomPanelHeightPx;
39
28
  } catch {
40
29
  return kDefaultBrowseBottomPanelHeightPx;
41
30
  }
@@ -55,6 +44,7 @@ export const AppSidebar = ({
55
44
  onSelectNavItem = () => {},
56
45
  selectedBrowsePath = "",
57
46
  onSelectBrowseFile = () => {},
47
+ onPreviewBrowseFile = () => {},
58
48
  acHasUpdate = false,
59
49
  acLatest = "",
60
50
  acDismissed = false,
@@ -70,18 +60,9 @@ export const AppSidebar = ({
70
60
  const [isResizingBrowsePanels, setIsResizingBrowsePanels] = useState(false);
71
61
 
72
62
  useEffect(() => {
73
- try {
74
- const rawSettings =
75
- window.localStorage.getItem(kUiSettingsStorageKey) ||
76
- window.localStorage.getItem(kLegacyUiSettingsStorageKey);
77
- const parsedSettings = rawSettings ? JSON.parse(rawSettings) : {};
78
- const nextSettings =
79
- parsedSettings && typeof parsedSettings === "object"
80
- ? { ...parsedSettings }
81
- : {};
82
- nextSettings[kBrowseBottomPanelUiSettingKey] = browseBottomPanelHeightPx;
83
- window.localStorage.setItem(kUiSettingsStorageKey, JSON.stringify(nextSettings));
84
- } catch {}
63
+ const settings = readUiSettings();
64
+ settings[kBrowseBottomPanelUiSettingKey] = browseBottomPanelHeightPx;
65
+ writeUiSettings(settings);
85
66
  }, [browseBottomPanelHeightPx]);
86
67
 
87
68
  const getClampedBrowseBottomPanelHeight = (value) => {
@@ -217,6 +198,7 @@ export const AppSidebar = ({
217
198
  <${FileTree}
218
199
  onSelectFile=${onSelectBrowseFile}
219
200
  selectedPath=${selectedBrowsePath}
201
+ onPreviewFile=${onPreviewBrowseFile}
220
202
  />
221
203
  </div>
222
204
  <div