@chrysb/alphaclaw 0.3.2 → 0.3.4-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 (54) hide show
  1. package/bin/alphaclaw.js +47 -2
  2. package/lib/cli/git-sync.js +25 -0
  3. package/lib/plugin/usage-tracker/index.js +308 -0
  4. package/lib/plugin/usage-tracker/openclaw.plugin.json +8 -0
  5. package/lib/public/css/explorer.css +1033 -0
  6. package/lib/public/css/shell.css +50 -4
  7. package/lib/public/css/theme.css +41 -1
  8. package/lib/public/icons/folder-line.svg +1 -0
  9. package/lib/public/icons/hashtag.svg +3 -0
  10. package/lib/public/icons/home-5-line.svg +1 -0
  11. package/lib/public/icons/save-fill.svg +3 -0
  12. package/lib/public/js/app.js +310 -160
  13. package/lib/public/js/components/action-button.js +12 -1
  14. package/lib/public/js/components/file-tree.js +497 -0
  15. package/lib/public/js/components/file-viewer.js +714 -0
  16. package/lib/public/js/components/icons.js +182 -0
  17. package/lib/public/js/components/segmented-control.js +33 -0
  18. package/lib/public/js/components/sidebar-git-panel.js +149 -0
  19. package/lib/public/js/components/sidebar.js +254 -0
  20. package/lib/public/js/components/telegram-workspace/index.js +353 -0
  21. package/lib/public/js/components/telegram-workspace/manage.js +397 -0
  22. package/lib/public/js/components/telegram-workspace/onboarding.js +616 -0
  23. package/lib/public/js/components/usage-tab.js +528 -0
  24. package/lib/public/js/components/watchdog-tab.js +1 -1
  25. package/lib/public/js/lib/api.js +51 -1
  26. package/lib/public/js/lib/browse-draft-state.js +109 -0
  27. package/lib/public/js/lib/file-highlighting.js +6 -0
  28. package/lib/public/js/lib/file-tree-utils.js +12 -0
  29. package/lib/public/js/lib/syntax-highlighters/css.js +124 -0
  30. package/lib/public/js/lib/syntax-highlighters/frontmatter.js +49 -0
  31. package/lib/public/js/lib/syntax-highlighters/html.js +209 -0
  32. package/lib/public/js/lib/syntax-highlighters/index.js +28 -0
  33. package/lib/public/js/lib/syntax-highlighters/javascript.js +134 -0
  34. package/lib/public/js/lib/syntax-highlighters/json.js +61 -0
  35. package/lib/public/js/lib/syntax-highlighters/markdown.js +37 -0
  36. package/lib/public/js/lib/syntax-highlighters/utils.js +13 -0
  37. package/lib/public/js/lib/telegram-api.js +78 -0
  38. package/lib/public/js/lib/ui-settings.js +38 -0
  39. package/lib/public/setup.html +34 -29
  40. package/lib/server/alphaclaw-version.js +3 -3
  41. package/lib/server/constants.js +2 -0
  42. package/lib/server/onboarding/openclaw.js +15 -0
  43. package/lib/server/onboarding/workspace.js +3 -2
  44. package/lib/server/routes/auth.js +5 -1
  45. package/lib/server/routes/browse.js +295 -0
  46. package/lib/server/routes/telegram.js +185 -60
  47. package/lib/server/routes/usage.js +133 -0
  48. package/lib/server/usage-db.js +570 -0
  49. package/lib/server.js +45 -4
  50. package/lib/setup/core-prompts/AGENTS.md +0 -101
  51. package/lib/setup/core-prompts/TOOLS.md +3 -1
  52. package/lib/setup/skills/control-ui/SKILL.md +12 -20
  53. package/package.json +1 -1
  54. package/lib/public/js/components/telegram-workspace.js +0 -1365
@@ -0,0 +1,714 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "https://esm.sh/preact/hooks";
9
+ import htm from "https://esm.sh/htm";
10
+ import { marked } from "https://esm.sh/marked";
11
+ import { fetchFileContent, saveFileContent } from "../lib/api.js";
12
+ import {
13
+ formatFrontmatterValue,
14
+ getFileSyntaxKind,
15
+ highlightEditorLines,
16
+ parseFrontmatter,
17
+ } from "../lib/syntax-highlighters/index.js";
18
+ import {
19
+ clearStoredFileDraft,
20
+ readStoredFileDraft,
21
+ updateDraftIndex,
22
+ writeStoredFileDraft,
23
+ } from "../lib/browse-draft-state.js";
24
+ import { ActionButton } from "./action-button.js";
25
+ import { LoadingSpinner } from "./loading-spinner.js";
26
+ import { SegmentedControl } from "./segmented-control.js";
27
+ import { SaveFillIcon } from "./icons.js";
28
+ import { showToast } from "./toast.js";
29
+
30
+ const html = htm.bind(h);
31
+ const kFileViewerModeStorageKey = "alphaclaw.browse.fileViewerMode";
32
+ const kLegacyFileViewerModeStorageKey = "alphaclawBrowseFileViewerMode";
33
+ const kProtectedBrowsePaths = new Set(["openclaw.json", "devices/paired.json"]);
34
+ const kLoadingIndicatorDelayMs = 1000;
35
+
36
+ const parsePathSegments = (inputPath) =>
37
+ String(inputPath || "")
38
+ .split("/")
39
+ .map((part) => part.trim())
40
+ .filter(Boolean);
41
+
42
+ const normalizePolicyPath = (inputPath) =>
43
+ String(inputPath || "")
44
+ .replaceAll("\\", "/")
45
+ .replace(/^\.\/+/, "")
46
+ .replace(/^\/+/, "")
47
+ .trim()
48
+ .toLowerCase();
49
+
50
+ const readStoredFileViewerMode = () => {
51
+ try {
52
+ const storedMode = String(
53
+ window.localStorage.getItem(kFileViewerModeStorageKey) ||
54
+ window.localStorage.getItem(kLegacyFileViewerModeStorageKey) ||
55
+ "",
56
+ ).trim();
57
+ return storedMode === "preview" ? "preview" : "edit";
58
+ } catch {
59
+ return "edit";
60
+ }
61
+ };
62
+
63
+
64
+ export const FileViewer = ({
65
+ filePath = "",
66
+ isPreviewOnly = false,
67
+ }) => {
68
+ const normalizedPath = String(filePath || "").trim();
69
+ const normalizedPolicyPath = normalizePolicyPath(normalizedPath);
70
+ const [content, setContent] = useState("");
71
+ const [initialContent, setInitialContent] = useState("");
72
+ const [viewMode, setViewMode] = useState(readStoredFileViewerMode);
73
+ const [loading, setLoading] = useState(false);
74
+ const [showDelayedLoadingSpinner, setShowDelayedLoadingSpinner] =
75
+ useState(false);
76
+ const [saving, setSaving] = useState(false);
77
+ const [error, setError] = useState("");
78
+ const [isFolderPath, setIsFolderPath] = useState(false);
79
+ const [frontmatterCollapsed, setFrontmatterCollapsed] = useState(false);
80
+ const [protectedEditBypassPaths, setProtectedEditBypassPaths] = useState(
81
+ () => new Set(),
82
+ );
83
+ const editorLineNumbersRef = useRef(null);
84
+ const editorHighlightRef = useRef(null);
85
+ const editorTextareaRef = useRef(null);
86
+ const previewRef = useRef(null);
87
+ const viewScrollRatioRef = useRef(0);
88
+ const isSyncingScrollRef = useRef(false);
89
+ const loadedFilePathRef = useRef("");
90
+ const editorLineNumberRowRefs = useRef([]);
91
+ const editorHighlightLineRefs = useRef([]);
92
+
93
+ const pathSegments = useMemo(
94
+ () => parsePathSegments(normalizedPath),
95
+ [normalizedPath],
96
+ );
97
+ const hasSelectedPath = normalizedPath.length > 0;
98
+ const canEditFile = hasSelectedPath && !isFolderPath && !isPreviewOnly;
99
+ const isDirty = canEditFile && content !== initialContent;
100
+ const isProtectedFile =
101
+ canEditFile && kProtectedBrowsePaths.has(normalizedPolicyPath);
102
+ const isProtectedLocked =
103
+ isProtectedFile && !protectedEditBypassPaths.has(normalizedPolicyPath);
104
+ const syntaxKind = useMemo(
105
+ () => getFileSyntaxKind(normalizedPath),
106
+ [normalizedPath],
107
+ );
108
+ const isMarkdownFile = syntaxKind === "markdown";
109
+ const shouldUseHighlightedEditor = syntaxKind !== "plain";
110
+ const parsedFrontmatter = useMemo(
111
+ () =>
112
+ isMarkdownFile
113
+ ? parseFrontmatter(content)
114
+ : { entries: [], body: content },
115
+ [content, isMarkdownFile],
116
+ );
117
+ const highlightedEditorLines = useMemo(
118
+ () =>
119
+ shouldUseHighlightedEditor
120
+ ? highlightEditorLines(content, syntaxKind)
121
+ : [],
122
+ [content, shouldUseHighlightedEditor, syntaxKind],
123
+ );
124
+ const editorLineNumbers = useMemo(() => {
125
+ const lineCount = String(content || "").split("\n").length;
126
+ return Array.from({ length: lineCount }, (_, index) => index + 1);
127
+ }, [content]);
128
+
129
+ const syncEditorLineNumberHeights = useCallback(() => {
130
+ if (!shouldUseHighlightedEditor || viewMode !== "edit") return;
131
+ const numberRows = editorLineNumberRowRefs.current;
132
+ const highlightRows = editorHighlightLineRefs.current;
133
+ const rowCount = Math.min(numberRows.length, highlightRows.length);
134
+ for (let index = 0; index < rowCount; index += 1) {
135
+ const numberRow = numberRows[index];
136
+ const highlightRow = highlightRows[index];
137
+ if (!numberRow || !highlightRow) continue;
138
+ numberRow.style.height = `${highlightRow.offsetHeight}px`;
139
+ }
140
+ }, [shouldUseHighlightedEditor, viewMode]);
141
+
142
+ useEffect(() => {
143
+ syncEditorLineNumberHeights();
144
+ }, [content, syncEditorLineNumberHeights]);
145
+
146
+ useEffect(() => {
147
+ if (!shouldUseHighlightedEditor || viewMode !== "edit") return () => {};
148
+ const onResize = () => syncEditorLineNumberHeights();
149
+ window.addEventListener("resize", onResize);
150
+ return () => window.removeEventListener("resize", onResize);
151
+ }, [shouldUseHighlightedEditor, viewMode, syncEditorLineNumberHeights]);
152
+ const previewHtml = useMemo(
153
+ () =>
154
+ isMarkdownFile
155
+ ? marked.parse(parsedFrontmatter.body || "", {
156
+ gfm: true,
157
+ breaks: true,
158
+ })
159
+ : "",
160
+ [parsedFrontmatter.body, isMarkdownFile],
161
+ );
162
+
163
+ useEffect(() => {
164
+ if (!isMarkdownFile && viewMode !== "edit") {
165
+ setViewMode("edit");
166
+ }
167
+ }, [isMarkdownFile, viewMode]);
168
+
169
+ useEffect(() => {
170
+ try {
171
+ window.localStorage.setItem(kFileViewerModeStorageKey, viewMode);
172
+ } catch {}
173
+ }, [viewMode]);
174
+
175
+ useEffect(() => {
176
+ if (!loading) {
177
+ setShowDelayedLoadingSpinner(false);
178
+ return () => {};
179
+ }
180
+ const timer = window.setTimeout(() => {
181
+ setShowDelayedLoadingSpinner(true);
182
+ }, kLoadingIndicatorDelayMs);
183
+ return () => window.clearTimeout(timer);
184
+ }, [loading]);
185
+
186
+ useEffect(() => {
187
+ let active = true;
188
+ loadedFilePathRef.current = "";
189
+ if (!hasSelectedPath) {
190
+ setContent("");
191
+ setInitialContent("");
192
+ setError("");
193
+ setIsFolderPath(false);
194
+ viewScrollRatioRef.current = 0;
195
+ loadedFilePathRef.current = "";
196
+ return () => {
197
+ active = false;
198
+ };
199
+ }
200
+
201
+ const loadFile = async () => {
202
+ setLoading(true);
203
+ setError("");
204
+ setIsFolderPath(false);
205
+ try {
206
+ const data = await fetchFileContent(normalizedPath);
207
+ if (!active) return;
208
+ const nextContent = data.content || "";
209
+ const draftContent = readStoredFileDraft(normalizedPath);
210
+ setContent(draftContent || nextContent);
211
+ updateDraftIndex(
212
+ normalizedPath,
213
+ Boolean(draftContent && draftContent !== nextContent),
214
+ { dispatchEvent: (event) => window.dispatchEvent(event) },
215
+ );
216
+ setInitialContent(nextContent);
217
+ viewScrollRatioRef.current = 0;
218
+ loadedFilePathRef.current = normalizedPath;
219
+ } catch (loadError) {
220
+ if (!active) return;
221
+ const message = loadError.message || "Could not load file";
222
+ if (/path is not a file/i.test(message)) {
223
+ setContent("");
224
+ setInitialContent("");
225
+ setIsFolderPath(true);
226
+ setError("");
227
+ loadedFilePathRef.current = normalizedPath;
228
+ return;
229
+ }
230
+ setError(message);
231
+ } finally {
232
+ if (active) setLoading(false);
233
+ }
234
+ };
235
+ loadFile();
236
+ return () => {
237
+ active = false;
238
+ };
239
+ }, [hasSelectedPath, normalizedPath]);
240
+
241
+ useEffect(() => {
242
+ if (loadedFilePathRef.current !== normalizedPath) return;
243
+ if (!canEditFile || !hasSelectedPath || loading) return;
244
+ if (content === initialContent) {
245
+ clearStoredFileDraft(normalizedPath);
246
+ updateDraftIndex(normalizedPath, false, {
247
+ dispatchEvent: (event) => window.dispatchEvent(event),
248
+ });
249
+ return;
250
+ }
251
+ writeStoredFileDraft(normalizedPath, content);
252
+ updateDraftIndex(normalizedPath, true, {
253
+ dispatchEvent: (event) => window.dispatchEvent(event),
254
+ });
255
+ }, [
256
+ canEditFile,
257
+ hasSelectedPath,
258
+ loading,
259
+ content,
260
+ initialContent,
261
+ normalizedPath,
262
+ ]);
263
+
264
+ const handleSave = async () => {
265
+ if (!canEditFile || saving || !isDirty || isProtectedLocked) return;
266
+ setSaving(true);
267
+ setError("");
268
+ try {
269
+ const data = await saveFileContent(normalizedPath, content);
270
+ setInitialContent(content);
271
+ clearStoredFileDraft(normalizedPath);
272
+ updateDraftIndex(normalizedPath, false, {
273
+ dispatchEvent: (event) => window.dispatchEvent(event),
274
+ });
275
+ window.dispatchEvent(
276
+ new CustomEvent("alphaclaw:browse-file-saved", {
277
+ detail: { path: normalizedPath },
278
+ }),
279
+ );
280
+ if (data.synced === false) {
281
+ showToast("Saved, but git sync failed", "error");
282
+ } else {
283
+ showToast("Saved and synced", "success");
284
+ }
285
+ } catch (saveError) {
286
+ const message = saveError.message || "Could not save file";
287
+ setError(message);
288
+ showToast(message, "error");
289
+ } finally {
290
+ setSaving(false);
291
+ }
292
+ };
293
+
294
+ const handleEditProtectedFile = () => {
295
+ if (!normalizedPolicyPath) return;
296
+ setProtectedEditBypassPaths((previousPaths) => {
297
+ const nextPaths = new Set(previousPaths);
298
+ nextPaths.add(normalizedPolicyPath);
299
+ return nextPaths;
300
+ });
301
+ };
302
+
303
+ const handleContentInput = (event) => {
304
+ if (isProtectedLocked || isPreviewOnly) return;
305
+ const nextContent = event.target.value;
306
+ setContent(nextContent);
307
+ if (hasSelectedPath && canEditFile) {
308
+ writeStoredFileDraft(normalizedPath, nextContent);
309
+ updateDraftIndex(normalizedPath, nextContent !== initialContent, {
310
+ dispatchEvent: (event) => window.dispatchEvent(event),
311
+ });
312
+ }
313
+ };
314
+
315
+ const getScrollRatio = (element) => {
316
+ if (!element) return 0;
317
+ const maxScrollTop = element.scrollHeight - element.clientHeight;
318
+ if (maxScrollTop <= 0) return 0;
319
+ return element.scrollTop / maxScrollTop;
320
+ };
321
+
322
+ const setScrollByRatio = (element, ratio) => {
323
+ if (!element) return;
324
+ const maxScrollTop = element.scrollHeight - element.clientHeight;
325
+ if (maxScrollTop <= 0) {
326
+ element.scrollTop = 0;
327
+ return;
328
+ }
329
+ const clampedRatio = Math.max(0, Math.min(1, ratio));
330
+ element.scrollTop = maxScrollTop * clampedRatio;
331
+ };
332
+
333
+ const handleEditorScroll = (event) => {
334
+ if (isSyncingScrollRef.current) return;
335
+ const nextScrollTop = event.currentTarget.scrollTop;
336
+ const nextRatio = getScrollRatio(event.currentTarget);
337
+ viewScrollRatioRef.current = nextRatio;
338
+ if (!editorLineNumbersRef.current) return;
339
+ editorLineNumbersRef.current.scrollTop = nextScrollTop;
340
+ if (editorHighlightRef.current) {
341
+ editorHighlightRef.current.scrollTop = nextScrollTop;
342
+ editorHighlightRef.current.scrollLeft = event.currentTarget.scrollLeft;
343
+ }
344
+ if (previewRef.current) {
345
+ isSyncingScrollRef.current = true;
346
+ setScrollByRatio(previewRef.current, nextRatio);
347
+ window.requestAnimationFrame(() => {
348
+ isSyncingScrollRef.current = false;
349
+ });
350
+ }
351
+ };
352
+
353
+ const handlePreviewScroll = (event) => {
354
+ if (isSyncingScrollRef.current) return;
355
+ const nextRatio = getScrollRatio(event.currentTarget);
356
+ viewScrollRatioRef.current = nextRatio;
357
+ isSyncingScrollRef.current = true;
358
+ setScrollByRatio(editorTextareaRef.current, nextRatio);
359
+ setScrollByRatio(editorLineNumbersRef.current, nextRatio);
360
+ setScrollByRatio(editorHighlightRef.current, nextRatio);
361
+ window.requestAnimationFrame(() => {
362
+ isSyncingScrollRef.current = false;
363
+ });
364
+ };
365
+
366
+ const handleChangeViewMode = (nextMode) => {
367
+ if (nextMode === viewMode) return;
368
+ const nextRatio =
369
+ viewMode === "preview"
370
+ ? getScrollRatio(previewRef.current)
371
+ : getScrollRatio(editorTextareaRef.current);
372
+ viewScrollRatioRef.current = nextRatio;
373
+ setViewMode(nextMode);
374
+ window.requestAnimationFrame(() => {
375
+ isSyncingScrollRef.current = true;
376
+ if (nextMode === "preview") {
377
+ setScrollByRatio(previewRef.current, nextRatio);
378
+ } else {
379
+ setScrollByRatio(editorTextareaRef.current, nextRatio);
380
+ setScrollByRatio(editorLineNumbersRef.current, nextRatio);
381
+ setScrollByRatio(editorHighlightRef.current, nextRatio);
382
+ }
383
+ window.requestAnimationFrame(() => {
384
+ isSyncingScrollRef.current = false;
385
+ });
386
+ });
387
+ };
388
+
389
+ if (!hasSelectedPath) {
390
+ return html`
391
+ <div class="file-viewer-empty">
392
+ <div class="file-viewer-empty-mark">[ ]</div>
393
+ <div class="file-viewer-empty-title">
394
+ Browse and edit files<br />Syncs to git
395
+ </div>
396
+ </div>
397
+ `;
398
+ }
399
+
400
+ return html`
401
+ <div class="file-viewer">
402
+ <div class="file-viewer-tabbar">
403
+ <div class="file-viewer-tab active">
404
+ <span class="file-icon">f</span>
405
+ <span class="file-viewer-breadcrumb">
406
+ ${pathSegments.map(
407
+ (segment, index) => html`
408
+ <span class="file-viewer-breadcrumb-item">
409
+ <span
410
+ class=${index === pathSegments.length - 1
411
+ ? "is-current"
412
+ : ""}
413
+ >
414
+ ${segment}
415
+ </span>
416
+ ${index < pathSegments.length - 1 &&
417
+ html`<span class="file-viewer-sep">></span>`}
418
+ </span>
419
+ `,
420
+ )}
421
+ </span>
422
+ ${isDirty
423
+ ? html`<span
424
+ class="file-viewer-dirty-dot"
425
+ aria-hidden="true"
426
+ ></span>`
427
+ : null}
428
+ </div>
429
+ <div class="file-viewer-tabbar-spacer"></div>
430
+ ${isPreviewOnly
431
+ ? html`<div class="file-viewer-preview-pill">Preview</div>`
432
+ : null}
433
+ ${isMarkdownFile &&
434
+ html`
435
+ <${SegmentedControl}
436
+ className="mr-2.5"
437
+ options=${[
438
+ { label: "edit", value: "edit" },
439
+ { label: "preview", value: "preview" },
440
+ ]}
441
+ value=${viewMode}
442
+ onChange=${handleChangeViewMode}
443
+ />
444
+ `}
445
+ <${ActionButton}
446
+ onClick=${handleSave}
447
+ disabled=${loading || !isDirty || !canEditFile || isProtectedLocked}
448
+ loading=${saving}
449
+ tone=${isDirty ? "primary" : "secondary"}
450
+ size="sm"
451
+ idleLabel="Save"
452
+ loadingLabel="Saving..."
453
+ idleIcon=${SaveFillIcon}
454
+ idleIconClassName="file-viewer-save-icon"
455
+ className="file-viewer-save-action"
456
+ />
457
+ </div>
458
+ ${isProtectedFile
459
+ ? html`
460
+ <div class="file-viewer-protected-banner">
461
+ <div class="file-viewer-protected-banner-text">
462
+ Protected file. Changes may break workspace behavior.
463
+ </div>
464
+ ${isProtectedLocked
465
+ ? html`
466
+ <${ActionButton}
467
+ onClick=${handleEditProtectedFile}
468
+ tone="warning"
469
+ size="sm"
470
+ idleLabel="Edit anyway"
471
+ />
472
+ `
473
+ : null}
474
+ </div>
475
+ `
476
+ : null}
477
+ ${isMarkdownFile && parsedFrontmatter.entries.length > 0
478
+ ? html`
479
+ <div class="frontmatter-box">
480
+ <button
481
+ type="button"
482
+ class="frontmatter-title"
483
+ onclick=${() =>
484
+ setFrontmatterCollapsed((collapsed) => !collapsed)}
485
+ >
486
+ <span
487
+ class=${`frontmatter-chevron ${frontmatterCollapsed ? "" : "open"}`}
488
+ aria-hidden="true"
489
+ >
490
+ <svg viewBox="0 0 20 20" focusable="false">
491
+ <path d="M7 4l6 6-6 6" />
492
+ </svg>
493
+ </span>
494
+ <span>frontmatter</span>
495
+ </button>
496
+ ${!frontmatterCollapsed
497
+ ? html`
498
+ <div class="frontmatter-grid">
499
+ ${parsedFrontmatter.entries.map((entry) => {
500
+ const formattedValue = formatFrontmatterValue(
501
+ entry.rawValue,
502
+ );
503
+ const isMultilineValue = formattedValue.includes("\n");
504
+ return html`
505
+ <div class="frontmatter-row" key=${entry.key}>
506
+ <div class="frontmatter-key">${entry.key}</div>
507
+ ${isMultilineValue
508
+ ? html`
509
+ <pre
510
+ class="frontmatter-value frontmatter-value-pre"
511
+ >
512
+ ${formattedValue}</pre
513
+ >
514
+ `
515
+ : html`<div class="frontmatter-value">
516
+ ${formattedValue}
517
+ </div>`}
518
+ </div>
519
+ `;
520
+ })}
521
+ </div>
522
+ `
523
+ : null}
524
+ </div>
525
+ `
526
+ : null}
527
+ ${loading
528
+ ? html`
529
+ <div class="file-viewer-loading-shell">
530
+ ${showDelayedLoadingSpinner
531
+ ? html`<${LoadingSpinner} className="h-4 w-4" />`
532
+ : null}
533
+ </div>
534
+ `
535
+ : error
536
+ ? html`<div class="file-viewer-state file-viewer-state-error">
537
+ ${error}
538
+ </div>`
539
+ : isFolderPath
540
+ ? html`
541
+ <div class="file-viewer-state">
542
+ Folder selected. Choose a file from this folder in the tree.
543
+ </div>
544
+ `
545
+ : html`
546
+ ${isMarkdownFile
547
+ ? html`
548
+ <div
549
+ class=${`file-viewer-preview ${viewMode === "preview" ? "" : "file-viewer-pane-hidden"}`}
550
+ ref=${previewRef}
551
+ onscroll=${handlePreviewScroll}
552
+ aria-hidden=${viewMode === "preview" ? "false" : "true"}
553
+ dangerouslySetInnerHTML=${{ __html: previewHtml }}
554
+ ></div>
555
+ <div
556
+ class=${`file-viewer-editor-shell ${viewMode === "edit" ? "" : "file-viewer-pane-hidden"}`}
557
+ aria-hidden=${viewMode === "edit" ? "false" : "true"}
558
+ >
559
+ <div
560
+ class="file-viewer-editor-line-num-col"
561
+ ref=${editorLineNumbersRef}
562
+ >
563
+ ${editorLineNumbers.map(
564
+ (lineNumber) => html`
565
+ <div
566
+ class="file-viewer-editor-line-num"
567
+ key=${lineNumber}
568
+ ref=${(element) => {
569
+ editorLineNumberRowRefs.current[
570
+ lineNumber - 1
571
+ ] = element;
572
+ }}
573
+ >
574
+ ${lineNumber}
575
+ </div>
576
+ `,
577
+ )}
578
+ </div>
579
+ <div class="file-viewer-editor-stack">
580
+ <div
581
+ class="file-viewer-editor-highlight"
582
+ ref=${editorHighlightRef}
583
+ >
584
+ ${highlightedEditorLines.map(
585
+ (line) => html`
586
+ <div
587
+ class="file-viewer-editor-highlight-line"
588
+ key=${line.lineNumber}
589
+ ref=${(element) => {
590
+ editorHighlightLineRefs.current[
591
+ line.lineNumber - 1
592
+ ] = element;
593
+ }}
594
+ >
595
+ <span
596
+ class="file-viewer-editor-highlight-line-content"
597
+ dangerouslySetInnerHTML=${{
598
+ __html: line.html,
599
+ }}
600
+ ></span>
601
+ </div>
602
+ `,
603
+ )}
604
+ </div>
605
+ <textarea
606
+ class="file-viewer-editor file-viewer-editor-overlay"
607
+ ref=${editorTextareaRef}
608
+ value=${content}
609
+ onInput=${handleContentInput}
610
+ onScroll=${handleEditorScroll}
611
+ spellcheck=${false}
612
+ autocorrect="off"
613
+ autocapitalize="off"
614
+ autocomplete="off"
615
+ data-gramm="false"
616
+ data-gramm_editor="false"
617
+ data-enable-grammarly="false"
618
+ wrap="soft"
619
+ ></textarea>
620
+ </div>
621
+ </div>
622
+ `
623
+ : html`
624
+ <div class="file-viewer-editor-shell">
625
+ <div
626
+ class="file-viewer-editor-line-num-col"
627
+ ref=${editorLineNumbersRef}
628
+ >
629
+ ${editorLineNumbers.map(
630
+ (lineNumber) => html`
631
+ <div
632
+ class="file-viewer-editor-line-num"
633
+ key=${lineNumber}
634
+ ref=${(element) => {
635
+ editorLineNumberRowRefs.current[
636
+ lineNumber - 1
637
+ ] = element;
638
+ }}
639
+ >
640
+ ${lineNumber}
641
+ </div>
642
+ `,
643
+ )}
644
+ </div>
645
+ ${shouldUseHighlightedEditor
646
+ ? html`
647
+ <div class="file-viewer-editor-stack">
648
+ <div
649
+ class="file-viewer-editor-highlight"
650
+ ref=${editorHighlightRef}
651
+ >
652
+ ${highlightedEditorLines.map(
653
+ (line) => html`
654
+ <div
655
+ class="file-viewer-editor-highlight-line"
656
+ key=${line.lineNumber}
657
+ ref=${(element) => {
658
+ editorHighlightLineRefs.current[
659
+ line.lineNumber - 1
660
+ ] = element;
661
+ }}
662
+ >
663
+ <span
664
+ class="file-viewer-editor-highlight-line-content"
665
+ dangerouslySetInnerHTML=${{
666
+ __html: line.html,
667
+ }}
668
+ ></span>
669
+ </div>
670
+ `,
671
+ )}
672
+ </div>
673
+ <textarea
674
+ class="file-viewer-editor file-viewer-editor-overlay"
675
+ ref=${editorTextareaRef}
676
+ value=${content}
677
+ onInput=${handleContentInput}
678
+ onScroll=${handleEditorScroll}
679
+ readonly=${isProtectedLocked || isPreviewOnly}
680
+ spellcheck=${false}
681
+ autocorrect="off"
682
+ autocapitalize="off"
683
+ autocomplete="off"
684
+ data-gramm="false"
685
+ data-gramm_editor="false"
686
+ data-enable-grammarly="false"
687
+ wrap="soft"
688
+ ></textarea>
689
+ </div>
690
+ `
691
+ : html`
692
+ <textarea
693
+ class="file-viewer-editor"
694
+ ref=${editorTextareaRef}
695
+ value=${content}
696
+ onInput=${handleContentInput}
697
+ onScroll=${handleEditorScroll}
698
+ readonly=${isProtectedLocked || isPreviewOnly}
699
+ spellcheck=${false}
700
+ autocorrect="off"
701
+ autocapitalize="off"
702
+ autocomplete="off"
703
+ data-gramm="false"
704
+ data-gramm_editor="false"
705
+ data-enable-grammarly="false"
706
+ wrap="soft"
707
+ ></textarea>
708
+ `}
709
+ </div>
710
+ `}
711
+ `}
712
+ </div>
713
+ `;
714
+ };