@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.
- package/lib/public/css/explorer.css +156 -4
- package/lib/public/js/components/file-tree.js +533 -48
- package/lib/public/js/components/file-viewer/index.js +2 -8
- package/lib/public/js/components/google/index.js +83 -48
- package/lib/public/js/components/icons.js +26 -0
- package/lib/public/js/components/onboarding/use-welcome-codex.js +129 -0
- package/lib/public/js/components/onboarding/use-welcome-pairing.js +74 -0
- package/lib/public/js/components/onboarding/use-welcome-storage.js +60 -0
- package/lib/public/js/components/sidebar.js +1 -1
- package/lib/public/js/components/telegram-workspace/onboarding.js +1 -1
- package/lib/public/js/components/webhooks.js +2 -2
- package/lib/public/js/components/welcome.js +57 -210
- package/lib/public/js/lib/api.js +27 -0
- package/lib/public/js/lib/browse-file-policies.js +3 -1
- package/lib/public/shared/browse-file-policies.json +2 -1
- package/lib/server/constants.js +0 -1
- package/lib/server/gmail-serve.js +1 -1
- package/lib/server/gmail-watch.js +8 -28
- package/lib/server/gog-skill.js +169 -0
- package/lib/server/onboarding/openclaw.js +9 -1
- package/lib/server/routes/browse/index.js +123 -6
- package/lib/server/routes/browse/path-utils.js +3 -1
- package/lib/server/routes/google.js +4 -0
- package/lib/server/routes/webhooks.js +3 -5
- package/lib/server/webhooks.js +1 -1
- package/lib/server.js +2 -0
- package/lib/setup/skills/gog-cli/calendar.md +63 -0
- package/lib/setup/skills/gog-cli/contacts.md +30 -0
- package/lib/setup/skills/gog-cli/docs.md +46 -0
- package/lib/setup/skills/gog-cli/drive.md +48 -0
- package/lib/setup/skills/gog-cli/gmail.md +64 -0
- package/lib/setup/skills/gog-cli/meet.md +6 -0
- package/lib/setup/skills/gog-cli/sheets.md +43 -0
- package/lib/setup/skills/gog-cli/tasks.md +32 -0
- 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 {
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
${
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
594
|
-
showToast("File deleted", "success");
|
|
864
|
+
handleSelectFile("");
|
|
865
|
+
showToast(deleteTargetIsFolder ? "Folder deleted" : "File deleted", "success");
|
|
595
866
|
setDeleteTargetPath("");
|
|
596
867
|
} catch (deleteError) {
|
|
597
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
715
|
-
|
|
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=${
|
|
725
|
-
|
|
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
|
|
736
|
-
message=${
|
|
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"
|