@chrysb/alphaclaw 0.4.1-beta.0 → 0.4.1-beta.2

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 (35) hide show
  1. package/lib/public/css/explorer.css +156 -4
  2. package/lib/public/js/components/file-tree.js +533 -48
  3. package/lib/public/js/components/file-viewer/index.js +2 -8
  4. package/lib/public/js/components/google/index.js +83 -48
  5. package/lib/public/js/components/icons.js +26 -0
  6. package/lib/public/js/components/onboarding/use-welcome-codex.js +129 -0
  7. package/lib/public/js/components/onboarding/use-welcome-pairing.js +74 -0
  8. package/lib/public/js/components/onboarding/use-welcome-storage.js +60 -0
  9. package/lib/public/js/components/sidebar.js +1 -1
  10. package/lib/public/js/components/telegram-workspace/onboarding.js +1 -1
  11. package/lib/public/js/components/webhooks.js +2 -2
  12. package/lib/public/js/components/welcome.js +57 -210
  13. package/lib/public/js/lib/api.js +27 -0
  14. package/lib/public/js/lib/browse-file-policies.js +3 -1
  15. package/lib/public/shared/browse-file-policies.json +2 -1
  16. package/lib/server/constants.js +0 -1
  17. package/lib/server/gmail-serve.js +1 -1
  18. package/lib/server/gmail-watch.js +8 -28
  19. package/lib/server/gog-skill.js +169 -0
  20. package/lib/server/onboarding/openclaw.js +9 -1
  21. package/lib/server/routes/browse/index.js +123 -6
  22. package/lib/server/routes/browse/path-utils.js +3 -1
  23. package/lib/server/routes/google.js +4 -0
  24. package/lib/server/routes/webhooks.js +3 -5
  25. package/lib/server/webhooks.js +1 -1
  26. package/lib/server.js +2 -0
  27. package/lib/setup/skills/gog-cli/calendar.md +63 -0
  28. package/lib/setup/skills/gog-cli/contacts.md +30 -0
  29. package/lib/setup/skills/gog-cli/docs.md +46 -0
  30. package/lib/setup/skills/gog-cli/drive.md +48 -0
  31. package/lib/setup/skills/gog-cli/gmail.md +64 -0
  32. package/lib/setup/skills/gog-cli/meet.md +6 -0
  33. package/lib/setup/skills/gog-cli/sheets.md +43 -0
  34. package/lib/setup/skills/gog-cli/tasks.md +32 -0
  35. package/package.json +2 -2
@@ -7,8 +7,13 @@ import {
7
7
  useState,
8
8
  } from "https://esm.sh/preact/hooks";
9
9
  import htm from "https://esm.sh/htm";
10
- import { fetchBrowseTree } from "../lib/api.js";
11
- import { deleteBrowseFile } from "../lib/api.js";
10
+ import {
11
+ fetchBrowseTree,
12
+ deleteBrowseFile,
13
+ createBrowseFile,
14
+ createBrowseFolder,
15
+ moveBrowsePath,
16
+ } from "../lib/api.js";
12
17
  import {
13
18
  kDraftIndexChangedEventName,
14
19
  readStoredDraftPaths,
@@ -32,6 +37,9 @@ import {
32
37
  Database2LineIcon,
33
38
  HashtagIcon,
34
39
  LockLineIcon,
40
+ FileAddLineIcon,
41
+ FolderAddLineIcon,
42
+ DeleteBinLineIcon,
35
43
  } from "./icons.js";
36
44
  import { LoadingSpinner } from "./loading-spinner.js";
37
45
  import { ConfirmDialog } from "./confirm-dialog.js";
@@ -208,6 +216,147 @@ const getFileIconMeta = (fileName) => {
208
216
  };
209
217
  };
210
218
 
219
+ const TreeContextMenu = ({
220
+ x,
221
+ y,
222
+ targetPath,
223
+ targetType,
224
+ isLocked,
225
+ onNewFile,
226
+ onNewFolder,
227
+ onDelete,
228
+ onClose,
229
+ }) => {
230
+ const menuRef = useRef(null);
231
+
232
+ useEffect(() => {
233
+ const handleClick = (e) => {
234
+ if (menuRef.current && !menuRef.current.contains(e.target)) onClose();
235
+ };
236
+ const handleKey = (e) => {
237
+ if (e.key === "Escape") onClose();
238
+ };
239
+ const handleScroll = () => onClose();
240
+ window.addEventListener("mousedown", handleClick);
241
+ window.addEventListener("keydown", handleKey);
242
+ window.addEventListener("scroll", handleScroll, true);
243
+ return () => {
244
+ window.removeEventListener("mousedown", handleClick);
245
+ window.removeEventListener("keydown", handleKey);
246
+ window.removeEventListener("scroll", handleScroll, true);
247
+ };
248
+ }, [onClose]);
249
+
250
+ const isFolder = targetType === "folder";
251
+ const isFile = targetType === "file";
252
+ const isRoot = targetType === "root";
253
+ const contextFolder = isFolder ? targetPath : "";
254
+ const canCreate = !isLocked && (isFolder || isRoot);
255
+ const canDelete = !isLocked && (isFolder || isFile) && targetPath;
256
+
257
+ return html`
258
+ <div
259
+ ref=${menuRef}
260
+ class="tree-context-menu"
261
+ style=${{ top: `${y}px`, left: `${x}px` }}
262
+ >
263
+ ${canCreate
264
+ ? html`
265
+ <button
266
+ class="tree-context-menu-item"
267
+ onclick=${() => { onNewFile(contextFolder); onClose(); }}
268
+ >
269
+ <${FileAddLineIcon} className="tree-context-menu-icon" />
270
+ <span>New File</span>
271
+ </button>
272
+ <button
273
+ class="tree-context-menu-item"
274
+ onclick=${() => { onNewFolder(contextFolder); onClose(); }}
275
+ >
276
+ <${FolderAddLineIcon} className="tree-context-menu-icon" />
277
+ <span>New Folder</span>
278
+ </button>
279
+ `
280
+ : null}
281
+ ${canDelete
282
+ ? html`
283
+ ${canCreate
284
+ ? html`<div class="tree-context-menu-sep"></div>`
285
+ : null}
286
+ <button
287
+ class="tree-context-menu-item"
288
+ onclick=${() => { onDelete(targetPath); onClose(); }}
289
+ >
290
+ <${DeleteBinLineIcon} className="tree-context-menu-icon" />
291
+ <span>Delete</span>
292
+ </button>
293
+ `
294
+ : null}
295
+ ${isLocked
296
+ ? html`
297
+ <div class="tree-context-menu-item is-disabled">
298
+ <${LockLineIcon} className="tree-context-menu-icon" />
299
+ <span>Managed by AlphaClaw</span>
300
+ </div>
301
+ `
302
+ : null}
303
+ </div>
304
+ `;
305
+ };
306
+
307
+ const CreationInput = ({ type, depth, onConfirm, onCancel }) => {
308
+ const inputRef = useRef(null);
309
+ const [value, setValue] = useState("");
310
+ const submittedRef = useRef(false);
311
+
312
+ useEffect(() => {
313
+ inputRef.current?.focus();
314
+ }, []);
315
+
316
+ const submit = () => {
317
+ const name = value.trim();
318
+ if (!name || submittedRef.current) {
319
+ onCancel();
320
+ return;
321
+ }
322
+ submittedRef.current = true;
323
+ onConfirm(name);
324
+ };
325
+
326
+ const IconComponent = type === "folder" ? FolderAddLineIcon : FileAddLineIcon;
327
+ return html`
328
+ <li class="tree-item">
329
+ <div
330
+ class="tree-create-row"
331
+ style=${{ paddingLeft: `${kFileBasePaddingPx + depth * kTreeIndentPx}px` }}
332
+ >
333
+ <${IconComponent} className="tree-create-icon" />
334
+ <input
335
+ ref=${inputRef}
336
+ class="tree-create-input"
337
+ type="text"
338
+ value=${value}
339
+ onInput=${(e) => setValue(e.target.value)}
340
+ onKeyDown=${(e) => {
341
+ if (e.key === "Enter") {
342
+ e.preventDefault();
343
+ submit();
344
+ }
345
+ if (e.key === "Escape") {
346
+ e.preventDefault();
347
+ onCancel();
348
+ }
349
+ }}
350
+ onBlur=${submit}
351
+ placeholder=${type === "folder" ? "folder name" : "file name"}
352
+ autocomplete="off"
353
+ spellcheck=${false}
354
+ />
355
+ </div>
356
+ </li>
357
+ `;
358
+ };
359
+
211
360
  const TreeNode = ({
212
361
  node,
213
362
  depth = 0,
@@ -216,10 +365,17 @@ const TreeNode = ({
216
365
  onSelectFolder,
217
366
  onRequestDelete,
218
367
  onSelectFile,
368
+ onContextMenu,
369
+ onDragDrop,
219
370
  selectedPath = "",
220
371
  draftPaths,
221
372
  isSearchActive = false,
222
373
  searchActivePath = "",
374
+ creatingInFolder = "",
375
+ creatingType = "",
376
+ onCreationConfirm,
377
+ onCreationCancel,
378
+ dragSourcePath = "",
223
379
  }) => {
224
380
  if (!node) return null;
225
381
  if (node.type === "file") {
@@ -233,10 +389,23 @@ const TreeNode = ({
233
389
  const fileIconMeta = getFileIconMeta(node.name);
234
390
  const FileTypeIcon = fileIconMeta.icon;
235
391
  return html`
236
- <li class="tree-item">
392
+ <li class="tree-item${dragSourcePath === node.path ? " is-dragging" : ""}">
237
393
  <a
238
394
  class=${`${isActive ? "active" : ""} ${isSearchActiveNode && !isActive ? "soft-active" : ""}`.trim()}
239
395
  onclick=${() => onSelectFile(node.path)}
396
+ oncontextmenu=${(e) => {
397
+ e.preventDefault();
398
+ e.stopPropagation();
399
+ onContextMenu({ x: e.clientX, y: e.clientY, targetPath: node.path, targetType: "file", isLocked });
400
+ }}
401
+ draggable=${!isLocked}
402
+ onDragStart=${(e) => {
403
+ if (isLocked) { e.preventDefault(); return; }
404
+ e.dataTransfer.setData("text/plain", node.path);
405
+ e.dataTransfer.effectAllowed = "move";
406
+ onDragDrop("start", node.path);
407
+ }}
408
+ onDragEnd=${() => onDragDrop("end", "")}
240
409
  onKeyDown=${(event) => {
241
410
  const isDeleteKey =
242
411
  event.key === "Delete" || event.key === "Backspace";
@@ -269,20 +438,60 @@ const TreeNode = ({
269
438
  const folderPath = node.path || "";
270
439
  const isCollapsed = isSearchActive ? false : !expandedPaths.has(folderPath);
271
440
  const isFolderActive = selectedPath === folderPath;
441
+ const isFolderLocked = folderPath && matchesBrowsePolicyPath(
442
+ kLockedBrowsePaths,
443
+ normalizeBrowsePolicyPath(folderPath),
444
+ );
445
+ const [isDropTarget, setIsDropTarget] = useState(false);
446
+ const dropCounterRef = useRef(0);
272
447
  return html`
273
- <li class="tree-item">
448
+ <li class="tree-item${dragSourcePath === folderPath ? " is-dragging" : ""}">
274
449
  <div
275
- class=${`tree-folder ${isCollapsed ? "collapsed" : ""} ${isFolderActive ? "active" : ""}`.trim()}
450
+ class=${`tree-folder ${isCollapsed ? "collapsed" : ""} ${isFolderActive ? "active" : ""} ${isDropTarget ? "is-drop-target" : ""}`.trim()}
276
451
  onclick=${() => {
277
452
  if (!folderPath) return;
278
- if (isFolderActive) {
279
- onSetFolderExpanded(folderPath, false);
280
- onSelectFolder("");
281
- return;
282
- }
283
- onSetFolderExpanded(folderPath, true);
453
+ onSetFolderExpanded(folderPath, isCollapsed);
284
454
  onSelectFolder(folderPath);
285
455
  }}
456
+ oncontextmenu=${(e) => {
457
+ e.preventDefault();
458
+ e.stopPropagation();
459
+ onContextMenu({ x: e.clientX, y: e.clientY, targetPath: folderPath, targetType: "folder", isLocked: isFolderLocked });
460
+ }}
461
+ draggable=${!!folderPath && !isFolderLocked}
462
+ onDragStart=${(e) => {
463
+ if (!folderPath || isFolderLocked) { e.preventDefault(); return; }
464
+ e.dataTransfer.setData("text/plain", folderPath);
465
+ e.dataTransfer.effectAllowed = "move";
466
+ onDragDrop("start", folderPath);
467
+ }}
468
+ onDragEnd=${() => { setIsDropTarget(false); dropCounterRef.current = 0; onDragDrop("end", ""); }}
469
+ onDragOver=${(e) => {
470
+ if (isFolderLocked) return;
471
+ e.preventDefault();
472
+ e.dataTransfer.dropEffect = "move";
473
+ }}
474
+ onDragEnter=${(e) => {
475
+ if (isFolderLocked) return;
476
+ e.preventDefault();
477
+ dropCounterRef.current += 1;
478
+ if (dropCounterRef.current === 1) setIsDropTarget(true);
479
+ }}
480
+ onDragLeave=${() => {
481
+ dropCounterRef.current -= 1;
482
+ if (dropCounterRef.current <= 0) { dropCounterRef.current = 0; setIsDropTarget(false); }
483
+ }}
484
+ onDrop=${(e) => {
485
+ if (isFolderLocked) return;
486
+ e.preventDefault();
487
+ e.stopPropagation();
488
+ setIsDropTarget(false);
489
+ dropCounterRef.current = 0;
490
+ const sourcePath = e.dataTransfer.getData("text/plain");
491
+ if (sourcePath && sourcePath !== folderPath) {
492
+ onDragDrop("drop", sourcePath, folderPath);
493
+ }
494
+ }}
286
495
  style=${{
287
496
  paddingLeft: `${kFolderBasePaddingPx + depth * kTreeIndentPx}px`,
288
497
  }}
@@ -297,19 +506,32 @@ const TreeNode = ({
297
506
  event.preventDefault();
298
507
  event.stopPropagation();
299
508
  if (!folderPath) return;
300
- const shouldCollapse = !isCollapsed;
301
- if (isFolderActive && shouldCollapse) {
302
- onSelectFolder("");
303
- }
304
509
  onSetFolderExpanded(folderPath, isCollapsed);
305
510
  }}
306
511
  >
307
512
  <span class="arrow">▼</span>
308
513
  </button>
309
514
  <span class="tree-label">${node.name}</span>
515
+ ${isFolderLocked
516
+ ? html`<${LockLineIcon}
517
+ className="tree-lock-icon"
518
+ title="Managed by AlphaClaw"
519
+ />`
520
+ : null}
310
521
  </div>
311
522
  <ul class=${`tree-children ${isCollapsed ? "hidden" : ""}`}>
312
- ${(node.children || []).map(
523
+ ${creatingInFolder === folderPath && creatingType === "folder"
524
+ ? html`
525
+ <${CreationInput}
526
+ key="__creation__"
527
+ type="folder"
528
+ depth=${depth + 1}
529
+ onConfirm=${onCreationConfirm}
530
+ onCancel=${onCreationCancel}
531
+ />
532
+ `
533
+ : null}
534
+ ${(node.children || []).filter((c) => c.type === "folder").map(
313
535
  (childNode) => html`
314
536
  <${TreeNode}
315
537
  key=${childNode.path || `${folderPath}/${childNode.name}`}
@@ -320,10 +542,53 @@ const TreeNode = ({
320
542
  onSelectFolder=${onSelectFolder}
321
543
  onRequestDelete=${onRequestDelete}
322
544
  onSelectFile=${onSelectFile}
545
+ onContextMenu=${onContextMenu}
546
+ onDragDrop=${onDragDrop}
323
547
  selectedPath=${selectedPath}
324
548
  draftPaths=${draftPaths}
325
549
  isSearchActive=${isSearchActive}
326
550
  searchActivePath=${searchActivePath}
551
+ creatingInFolder=${creatingInFolder}
552
+ creatingType=${creatingType}
553
+ onCreationConfirm=${onCreationConfirm}
554
+ onCreationCancel=${onCreationCancel}
555
+ dragSourcePath=${dragSourcePath}
556
+ />
557
+ `,
558
+ )}
559
+ ${creatingInFolder === folderPath && creatingType === "file"
560
+ ? html`
561
+ <${CreationInput}
562
+ key="__creation__"
563
+ type="file"
564
+ depth=${depth + 1}
565
+ onConfirm=${onCreationConfirm}
566
+ onCancel=${onCreationCancel}
567
+ />
568
+ `
569
+ : null}
570
+ ${(node.children || []).filter((c) => c.type !== "folder").map(
571
+ (childNode) => html`
572
+ <${TreeNode}
573
+ key=${childNode.path || `${folderPath}/${childNode.name}`}
574
+ node=${childNode}
575
+ depth=${depth + 1}
576
+ expandedPaths=${expandedPaths}
577
+ onSetFolderExpanded=${onSetFolderExpanded}
578
+ onSelectFolder=${onSelectFolder}
579
+ onRequestDelete=${onRequestDelete}
580
+ onSelectFile=${onSelectFile}
581
+ onContextMenu=${onContextMenu}
582
+ onDragDrop=${onDragDrop}
583
+ selectedPath=${selectedPath}
584
+ draftPaths=${draftPaths}
585
+ isSearchActive=${isSearchActive}
586
+ searchActivePath=${searchActivePath}
587
+ creatingInFolder=${creatingInFolder}
588
+ creatingType=${creatingType}
589
+ onCreationConfirm=${onCreationConfirm}
590
+ onCreationCancel=${onCreationCancel}
591
+ dragSourcePath=${dragSourcePath}
327
592
  />
328
593
  `,
329
594
  )}
@@ -347,6 +612,12 @@ export const FileTree = ({
347
612
  const [searchActivePath, setSearchActivePath] = useState("");
348
613
  const [deleteTargetPath, setDeleteTargetPath] = useState("");
349
614
  const [deletingFile, setDeletingFile] = useState(false);
615
+ const [creatingInFolder, setCreatingInFolder] = useState("");
616
+ const [creatingType, setCreatingType] = useState("");
617
+ const [contextMenu, setContextMenu] = useState(null);
618
+ const [dragSourcePath, setDragSourcePath] = useState("");
619
+ const [selectedFolder, setSelectedFolder] = useState("");
620
+ const effectiveSelectedPath = selectedFolder || selectedPath;
350
621
  const searchInputRef = useRef(null);
351
622
  const treeSignatureRef = useRef("");
352
623
 
@@ -443,19 +714,19 @@ export const FileTree = ({
443
714
  } catch {}
444
715
  }, [expandedPaths]);
445
716
 
717
+ useEffect(() => {
718
+ if (selectedPath) setSelectedFolder("");
719
+ }, [selectedPath]);
720
+
446
721
  useEffect(() => {
447
722
  if (!selectedPath) return;
448
723
  const ancestorFolderPaths = collectAncestorFolderPaths(selectedPath);
449
- const selectedIsFolder = folderPaths.has(selectedPath);
450
- const pathsToExpand = selectedIsFolder
451
- ? [...ancestorFolderPaths, selectedPath]
452
- : ancestorFolderPaths;
453
- if (!pathsToExpand.length) return;
724
+ if (!ancestorFolderPaths.length) return;
454
725
  setExpandedPaths((previousPaths) => {
455
726
  if (!(previousPaths instanceof Set)) return previousPaths;
456
727
  let didChange = false;
457
728
  const nextPaths = new Set(previousPaths);
458
- pathsToExpand.forEach((ancestorPath) => {
729
+ ancestorFolderPaths.forEach((ancestorPath) => {
459
730
  if (!nextPaths.has(ancestorPath)) {
460
731
  nextPaths.add(ancestorPath);
461
732
  didChange = true;
@@ -463,7 +734,7 @@ export const FileTree = ({
463
734
  });
464
735
  return didChange ? nextPaths : previousPaths;
465
736
  });
466
- }, [selectedPath, folderPaths]);
737
+ }, [selectedPath]);
467
738
 
468
739
  useEffect(() => {
469
740
  const handleDraftIndexChanged = (event) => {
@@ -547,30 +818,30 @@ export const FileTree = ({
547
818
  });
548
819
  };
549
820
 
821
+ const handleSelectFile = useCallback((filePath, options) => {
822
+ setSelectedFolder("");
823
+ onSelectFile(filePath, options);
824
+ }, [onSelectFile]);
825
+
550
826
  const selectFolder = (folderPath) => {
551
- onSelectFile(folderPath, {
552
- directory: true,
553
- preservePreview: true,
554
- });
827
+ setSelectedFolder(folderPath);
555
828
  };
556
829
 
557
830
  const requestDelete = (targetPath) => {
558
831
  const normalizedTargetPath = normalizeBrowsePolicyPath(targetPath);
559
832
  if (!normalizedTargetPath) return;
560
- if (!allTreeFilePaths.has(targetPath)) {
561
- showToast("Only files can be deleted", "warning");
562
- return;
563
- }
564
833
  if (
565
834
  matchesBrowsePolicyPath(kLockedBrowsePaths, normalizedTargetPath) ||
566
835
  matchesBrowsePolicyPath(kProtectedBrowsePaths, normalizedTargetPath)
567
836
  ) {
568
- showToast("Protected or locked files cannot be deleted", "warning");
837
+ showToast("Protected or locked paths cannot be deleted", "warning");
569
838
  return;
570
839
  }
571
840
  setDeleteTargetPath(targetPath);
572
841
  };
573
842
 
843
+ const deleteTargetIsFolder = folderPaths.has(deleteTargetPath);
844
+
574
845
  const confirmDelete = async () => {
575
846
  if (!deleteTargetPath || deletingFile) return;
576
847
  setDeletingFile(true);
@@ -590,21 +861,115 @@ export const FileTree = ({
590
861
  removeTreePath(previousRoot, deleteTargetPath),
591
862
  );
592
863
  window.dispatchEvent(new CustomEvent("alphaclaw:browse-tree-refresh"));
593
- onSelectFile("");
594
- showToast("File deleted", "success");
864
+ handleSelectFile("");
865
+ showToast(deleteTargetIsFolder ? "Folder deleted" : "File deleted", "success");
595
866
  setDeleteTargetPath("");
596
867
  } catch (deleteError) {
597
- const message = deleteError.message || "Could not delete file";
598
- if (/path is not a file/i.test(message)) {
599
- showToast("Only files can be deleted", "warning");
600
- } else {
601
- showToast(message, "error");
602
- }
868
+ showToast(deleteError.message || "Could not delete", "error");
603
869
  } finally {
604
870
  setDeletingFile(false);
605
871
  }
606
872
  };
607
873
 
874
+ const getCreateFolder = (explicitFolder) => {
875
+ if (explicitFolder !== undefined) return explicitFolder;
876
+ if (!effectiveSelectedPath) return "";
877
+ if (folderPaths.has(effectiveSelectedPath)) return effectiveSelectedPath;
878
+ const lastSlash = effectiveSelectedPath.lastIndexOf("/");
879
+ return lastSlash > 0 ? effectiveSelectedPath.slice(0, lastSlash) : "";
880
+ };
881
+
882
+ const requestCreate = (folderPath, type) => {
883
+ const target = getCreateFolder(folderPath);
884
+ if (target && matchesBrowsePolicyPath(kLockedBrowsePaths, normalizeBrowsePolicyPath(target))) {
885
+ showToast("Cannot create inside a locked folder", "warning");
886
+ return;
887
+ }
888
+ setCreatingInFolder(target);
889
+ setCreatingType(type);
890
+ if (target) {
891
+ setExpandedPaths((prev) => {
892
+ const next = prev instanceof Set ? new Set(prev) : new Set();
893
+ next.add(target);
894
+ return next;
895
+ });
896
+ }
897
+ };
898
+
899
+ const requestCreateFromToolbar = (type) => {
900
+ requestCreate(getCreateFolder(), type);
901
+ };
902
+
903
+ const cancelCreate = () => {
904
+ setCreatingInFolder("");
905
+ setCreatingType("");
906
+ };
907
+
908
+ const confirmCreate = async (name) => {
909
+ const folder = creatingInFolder;
910
+ const type = creatingType;
911
+ cancelCreate();
912
+ const fullPath = folder ? `${folder}/${name}` : name;
913
+ try {
914
+ if (type === "folder") {
915
+ await createBrowseFolder(fullPath);
916
+ showToast("Folder created", "success");
917
+ } else {
918
+ await createBrowseFile(fullPath);
919
+ showToast("File created", "success");
920
+ }
921
+ window.dispatchEvent(new CustomEvent("alphaclaw:browse-tree-refresh"));
922
+ if (folder) {
923
+ setExpandedPaths((prev) => {
924
+ const next = prev instanceof Set ? new Set(prev) : new Set();
925
+ next.add(folder);
926
+ return next;
927
+ });
928
+ }
929
+ if (type === "file") {
930
+ handleSelectFile(fullPath);
931
+ }
932
+ } catch (createError) {
933
+ showToast(createError.message || `Could not create ${type}`, "error");
934
+ }
935
+ };
936
+
937
+ const openContextMenu = (menu) => {
938
+ setContextMenu(menu);
939
+ };
940
+
941
+ const closeContextMenu = () => {
942
+ setContextMenu(null);
943
+ };
944
+
945
+ const handleDragDrop = async (action, sourcePath, targetFolder) => {
946
+ if (action === "start") {
947
+ setDragSourcePath(sourcePath);
948
+ return;
949
+ }
950
+ if (action === "end") {
951
+ setDragSourcePath("");
952
+ return;
953
+ }
954
+ if (action === "drop") {
955
+ setDragSourcePath("");
956
+ const basename = sourcePath.split("/").pop();
957
+ if (!basename) return;
958
+ const destination = targetFolder ? `${targetFolder}/${basename}` : basename;
959
+ if (sourcePath === destination) return;
960
+ try {
961
+ await moveBrowsePath(sourcePath, destination);
962
+ showToast(`Moved to ${targetFolder || "root"}`, "success");
963
+ window.dispatchEvent(new CustomEvent("alphaclaw:browse-tree-refresh"));
964
+ if (selectedPath === sourcePath) {
965
+ handleSelectFile(destination);
966
+ }
967
+ } catch (moveError) {
968
+ showToast(moveError.message || "Could not move", "error");
969
+ }
970
+ }
971
+ };
972
+
608
973
  const updateSearchQuery = (nextQuery) => {
609
974
  setSearchQuery(nextQuery);
610
975
  };
@@ -633,7 +998,7 @@ export const FileTree = ({
633
998
  const targetPath =
634
999
  searchActivePath || (filteredFilePaths.length === 1 ? singlePath : "");
635
1000
  if (!targetPath) return;
636
- onSelectFile(targetPath);
1001
+ handleSelectFile(targetPath);
637
1002
  clearSearch();
638
1003
  };
639
1004
 
@@ -673,7 +1038,7 @@ export const FileTree = ({
673
1038
  ${error}
674
1039
  </div>`;
675
1040
  }
676
- if (!rootChildren.length) {
1041
+ if (!rootChildren.length && !creatingType) {
677
1042
  return html`
678
1043
  <div class="file-tree-wrap">
679
1044
  <div class="file-tree-search">
@@ -688,6 +1053,24 @@ export const FileTree = ({
688
1053
  autocomplete="off"
689
1054
  spellcheck=${false}
690
1055
  />
1056
+ <span class="file-tree-search-actions">
1057
+ <button
1058
+ type="button"
1059
+ class="tree-folder-action"
1060
+ title="New file"
1061
+ onclick=${() => requestCreateFromToolbar("file")}
1062
+ >
1063
+ <${FileAddLineIcon} className="tree-folder-action-icon" />
1064
+ </button>
1065
+ <button
1066
+ type="button"
1067
+ class="tree-folder-action"
1068
+ title="New folder"
1069
+ onclick=${() => requestCreateFromToolbar("folder")}
1070
+ >
1071
+ <${FolderAddLineIcon} className="tree-folder-action-icon" />
1072
+ </button>
1073
+ </span>
691
1074
  </div>
692
1075
  <div class="file-tree-state">
693
1076
  ${isSearchActive ? "No matching files." : "No files found."}
@@ -710,9 +1093,51 @@ export const FileTree = ({
710
1093
  autocomplete="off"
711
1094
  spellcheck=${false}
712
1095
  />
1096
+ <span class="file-tree-search-actions">
1097
+ <button
1098
+ type="button"
1099
+ class="tree-folder-action"
1100
+ title="New file"
1101
+ onclick=${() => requestCreateFromToolbar("file")}
1102
+ >
1103
+ <${FileAddLineIcon} className="tree-folder-action-icon" />
1104
+ </button>
1105
+ <button
1106
+ type="button"
1107
+ class="tree-folder-action"
1108
+ title="New folder"
1109
+ onclick=${() => requestCreateFromToolbar("folder")}
1110
+ >
1111
+ <${FolderAddLineIcon} className="tree-folder-action-icon" />
1112
+ </button>
1113
+ </span>
713
1114
  </div>
714
- <ul class="file-tree">
715
- ${rootChildren.map(
1115
+ <div class="file-tree-scroll">
1116
+ <ul
1117
+ class="file-tree"
1118
+ oncontextmenu=${(e) => {
1119
+ e.preventDefault();
1120
+ openContextMenu({ x: e.clientX, y: e.clientY, targetPath: "", targetType: "root" });
1121
+ }}
1122
+ onDragOver=${(e) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; }}
1123
+ onDrop=${(e) => {
1124
+ e.preventDefault();
1125
+ const sourcePath = e.dataTransfer.getData("text/plain");
1126
+ if (sourcePath) handleDragDrop("drop", sourcePath, "");
1127
+ }}
1128
+ >
1129
+ ${creatingInFolder === "" && creatingType === "folder"
1130
+ ? html`
1131
+ <${CreationInput}
1132
+ key="__root-creation__"
1133
+ type="folder"
1134
+ depth=${0}
1135
+ onConfirm=${confirmCreate}
1136
+ onCancel=${cancelCreate}
1137
+ />
1138
+ `
1139
+ : null}
1140
+ ${rootChildren.filter((n) => n.type === "folder").map(
716
1141
  (node) => html`
717
1142
  <${TreeNode}
718
1143
  key=${node.path || node.name}
@@ -721,19 +1146,79 @@ export const FileTree = ({
721
1146
  onSetFolderExpanded=${setFolderExpanded}
722
1147
  onSelectFolder=${selectFolder}
723
1148
  onRequestDelete=${requestDelete}
724
- onSelectFile=${onSelectFile}
725
- selectedPath=${selectedPath}
1149
+ onSelectFile=${handleSelectFile}
1150
+ onContextMenu=${openContextMenu}
1151
+ onDragDrop=${handleDragDrop}
1152
+ selectedPath=${effectiveSelectedPath}
726
1153
  draftPaths=${draftPaths}
727
1154
  isSearchActive=${isSearchActive}
728
1155
  searchActivePath=${searchActivePath}
1156
+ creatingInFolder=${creatingInFolder}
1157
+ creatingType=${creatingType}
1158
+ onCreationConfirm=${confirmCreate}
1159
+ onCreationCancel=${cancelCreate}
1160
+ dragSourcePath=${dragSourcePath}
1161
+ />
1162
+ `,
1163
+ )}
1164
+ ${creatingInFolder === "" && creatingType === "file"
1165
+ ? html`
1166
+ <${CreationInput}
1167
+ key="__root-creation__"
1168
+ type="file"
1169
+ depth=${0}
1170
+ onConfirm=${confirmCreate}
1171
+ onCancel=${cancelCreate}
1172
+ />
1173
+ `
1174
+ : null}
1175
+ ${rootChildren.filter((n) => n.type !== "folder").map(
1176
+ (node) => html`
1177
+ <${TreeNode}
1178
+ key=${node.path || node.name}
1179
+ node=${node}
1180
+ expandedPaths=${safeExpandedPaths}
1181
+ onSetFolderExpanded=${setFolderExpanded}
1182
+ onSelectFolder=${selectFolder}
1183
+ onRequestDelete=${requestDelete}
1184
+ onSelectFile=${handleSelectFile}
1185
+ onContextMenu=${openContextMenu}
1186
+ onDragDrop=${handleDragDrop}
1187
+ selectedPath=${effectiveSelectedPath}
1188
+ draftPaths=${draftPaths}
1189
+ isSearchActive=${isSearchActive}
1190
+ searchActivePath=${searchActivePath}
1191
+ creatingInFolder=${creatingInFolder}
1192
+ creatingType=${creatingType}
1193
+ onCreationConfirm=${confirmCreate}
1194
+ onCreationCancel=${cancelCreate}
1195
+ dragSourcePath=${dragSourcePath}
729
1196
  />
730
1197
  `,
731
1198
  )}
732
1199
  </ul>
1200
+ </div>
1201
+ ${contextMenu
1202
+ ? html`
1203
+ <${TreeContextMenu}
1204
+ x=${contextMenu.x}
1205
+ y=${contextMenu.y}
1206
+ targetPath=${contextMenu.targetPath}
1207
+ targetType=${contextMenu.targetType}
1208
+ isLocked=${!!contextMenu.isLocked}
1209
+ onNewFile=${(folder) => requestCreate(folder, "file")}
1210
+ onNewFolder=${(folder) => requestCreate(folder, "folder")}
1211
+ onDelete=${requestDelete}
1212
+ onClose=${closeContextMenu}
1213
+ />
1214
+ `
1215
+ : null}
733
1216
  <${ConfirmDialog}
734
1217
  visible=${!!deleteTargetPath}
735
- title="Delete file?"
736
- message=${`Delete ${deleteTargetPath || "this file"}? This can be restored from diff view before sync.`}
1218
+ title=${deleteTargetIsFolder ? "Delete folder?" : "Delete file?"}
1219
+ message=${deleteTargetIsFolder
1220
+ ? `Delete folder ${deleteTargetPath || "this folder"} and all its contents?`
1221
+ : `Delete ${deleteTargetPath || "this file"}? This can be restored from diff view before sync.`}
737
1222
  confirmLabel="Delete"
738
1223
  confirmLoadingLabel="Deleting..."
739
1224
  cancelLabel="Cancel"