@chrysb/alphaclaw 0.3.4-beta.0 → 0.3.5-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.
- package/bin/alphaclaw.js +17 -2
- package/lib/public/css/explorer.css +184 -3
- package/lib/public/css/theme.css +1 -1
- package/lib/public/js/app.js +57 -7
- package/lib/public/js/components/file-tree.js +100 -25
- package/lib/public/js/components/file-viewer.js +543 -162
- package/lib/public/js/components/gateway.js +12 -18
- package/lib/public/js/components/icons.js +13 -0
- package/lib/public/js/components/sidebar-git-panel.js +155 -28
- package/lib/public/js/components/sidebar.js +1 -1
- package/lib/public/js/components/watchdog-tab.js +0 -2
- package/lib/public/js/lib/api.js +15 -0
- package/lib/server/helpers.js +18 -5
- package/lib/server/internal-files-migration.js +93 -0
- package/lib/server/onboarding/cron.js +6 -4
- package/lib/server/onboarding/index.js +7 -0
- package/lib/server/onboarding/openclaw.js +6 -1
- package/lib/server/routes/browse.js +233 -28
- package/lib/server/routes/pairings.js +8 -2
- package/lib/server/routes/system.js +5 -1
- package/lib/server.js +7 -0
- package/package.json +1 -1
package/bin/alphaclaw.js
CHANGED
|
@@ -10,6 +10,9 @@ const {
|
|
|
10
10
|
validateGitSyncFilePath,
|
|
11
11
|
} = require("../lib/cli/git-sync");
|
|
12
12
|
const { buildSecretReplacements } = require("../lib/server/helpers");
|
|
13
|
+
const {
|
|
14
|
+
migrateManagedInternalFiles,
|
|
15
|
+
} = require("../lib/server/internal-files-migration");
|
|
13
16
|
|
|
14
17
|
const kUsageTrackerPluginPath = path.resolve(
|
|
15
18
|
__dirname,
|
|
@@ -143,6 +146,10 @@ if (portFlag) {
|
|
|
143
146
|
|
|
144
147
|
const openclawDir = path.join(rootDir, ".openclaw");
|
|
145
148
|
fs.mkdirSync(openclawDir, { recursive: true });
|
|
149
|
+
const { hourlyGitSyncPath } = migrateManagedInternalFiles({
|
|
150
|
+
fs,
|
|
151
|
+
openclawDir,
|
|
152
|
+
});
|
|
146
153
|
console.log(`[alphaclaw] Root directory: ${rootDir}`);
|
|
147
154
|
|
|
148
155
|
// Check for pending update marker (written by the update endpoint before restart).
|
|
@@ -542,7 +549,6 @@ if (!fs.existsSync(gogConfigFile)) {
|
|
|
542
549
|
// 8. Install/reconcile system cron entry
|
|
543
550
|
// ---------------------------------------------------------------------------
|
|
544
551
|
|
|
545
|
-
const hourlyGitSyncPath = path.join(openclawDir, "hourly-git-sync.sh");
|
|
546
552
|
const packagedHourlyGitSyncPath = path.join(setupDir, "hourly-git-sync.sh");
|
|
547
553
|
|
|
548
554
|
try {
|
|
@@ -822,7 +828,16 @@ if (fs.existsSync(configPath)) {
|
|
|
822
828
|
const replacements = buildSecretReplacements(process.env);
|
|
823
829
|
for (const [secret, envRef] of replacements) {
|
|
824
830
|
if (secret) {
|
|
825
|
-
|
|
831
|
+
// Only replace the secret if it is an exact match for a JSON string value
|
|
832
|
+
// This ensures we do not replace substrings inside other strings
|
|
833
|
+
const secretJson = JSON.stringify(secret);
|
|
834
|
+
content = content.replace(
|
|
835
|
+
new RegExp(
|
|
836
|
+
secretJson.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"),
|
|
837
|
+
"g",
|
|
838
|
+
),
|
|
839
|
+
JSON.stringify(envRef),
|
|
840
|
+
);
|
|
826
841
|
}
|
|
827
842
|
}
|
|
828
843
|
fs.writeFileSync(configPath, content);
|
|
@@ -105,6 +105,11 @@
|
|
|
105
105
|
padding: 6px 0 8px;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
.file-tree-wrap-loading {
|
|
109
|
+
min-height: 100%;
|
|
110
|
+
display: flex;
|
|
111
|
+
}
|
|
112
|
+
|
|
108
113
|
.file-tree-search {
|
|
109
114
|
padding: 0 8px 6px;
|
|
110
115
|
}
|
|
@@ -281,6 +286,14 @@
|
|
|
281
286
|
box-shadow: 0 0 6px rgba(45, 226, 255, 0.75);
|
|
282
287
|
}
|
|
283
288
|
|
|
289
|
+
.tree-lock-icon {
|
|
290
|
+
flex: 0 0 auto;
|
|
291
|
+
margin-left: auto;
|
|
292
|
+
width: 11px;
|
|
293
|
+
height: 11px;
|
|
294
|
+
color: var(--text-dim);
|
|
295
|
+
}
|
|
296
|
+
|
|
284
297
|
.tree-children {
|
|
285
298
|
list-style: none;
|
|
286
299
|
}
|
|
@@ -295,6 +308,15 @@
|
|
|
295
308
|
color: var(--text-muted);
|
|
296
309
|
}
|
|
297
310
|
|
|
311
|
+
.file-tree-state-loading {
|
|
312
|
+
width: 100%;
|
|
313
|
+
flex: 1 1 auto;
|
|
314
|
+
min-height: 100%;
|
|
315
|
+
display: flex;
|
|
316
|
+
align-items: center;
|
|
317
|
+
justify-content: center;
|
|
318
|
+
}
|
|
319
|
+
|
|
298
320
|
.file-tree-state-error {
|
|
299
321
|
color: #f87171;
|
|
300
322
|
}
|
|
@@ -331,6 +353,21 @@
|
|
|
331
353
|
background: rgba(234, 179, 8, 0.08);
|
|
332
354
|
}
|
|
333
355
|
|
|
356
|
+
.file-viewer-protected-banner.is-locked {
|
|
357
|
+
background: rgba(220, 38, 38, 0.16);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.file-viewer-protected-banner-icon {
|
|
361
|
+
width: 14px;
|
|
362
|
+
height: 14px;
|
|
363
|
+
color: #fca5a5;
|
|
364
|
+
flex-shrink: 0;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.file-viewer-protected-banner.is-locked .file-viewer-protected-banner-text {
|
|
368
|
+
color: #fecaca;
|
|
369
|
+
}
|
|
370
|
+
|
|
334
371
|
.file-viewer-protected-banner-text {
|
|
335
372
|
font-size: 12px;
|
|
336
373
|
color: #f7cc5e;
|
|
@@ -344,6 +381,15 @@
|
|
|
344
381
|
letter-spacing: 0.01em;
|
|
345
382
|
}
|
|
346
383
|
|
|
384
|
+
.file-viewer-diff-banner {
|
|
385
|
+
background: rgba(59, 130, 246, 0.12);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.file-viewer-diff-banner .file-viewer-protected-banner-text,
|
|
389
|
+
.file-viewer-diff-banner {
|
|
390
|
+
color: #bfdbfe;
|
|
391
|
+
}
|
|
392
|
+
|
|
347
393
|
.file-viewer-tabbar-spacer {
|
|
348
394
|
flex: 1;
|
|
349
395
|
}
|
|
@@ -559,6 +605,48 @@
|
|
|
559
605
|
align-items: stretch;
|
|
560
606
|
}
|
|
561
607
|
|
|
608
|
+
.file-viewer-diff-shell {
|
|
609
|
+
width: 100%;
|
|
610
|
+
min-height: 0;
|
|
611
|
+
height: 100%;
|
|
612
|
+
overflow: auto;
|
|
613
|
+
padding: 8px 0;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.file-viewer-diff-pre {
|
|
617
|
+
margin: 0;
|
|
618
|
+
padding: 0 12px 12px;
|
|
619
|
+
font-family: inherit;
|
|
620
|
+
font-size: 12px;
|
|
621
|
+
line-height: 1.45;
|
|
622
|
+
color: var(--text-muted);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
.file-viewer-diff-line {
|
|
626
|
+
white-space: pre-wrap;
|
|
627
|
+
word-break: break-word;
|
|
628
|
+
padding: 1px 8px;
|
|
629
|
+
border-radius: 4px;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.file-viewer-diff-line.is-added {
|
|
633
|
+
color: #86efac;
|
|
634
|
+
background: rgba(34, 197, 94, 0.1);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.file-viewer-diff-line.is-removed {
|
|
638
|
+
color: #fca5a5;
|
|
639
|
+
background: rgba(239, 68, 68, 0.1);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.file-viewer-diff-line.is-hunk {
|
|
643
|
+
color: #93c5fd;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.file-viewer-diff-line.is-header {
|
|
647
|
+
color: var(--text-dim);
|
|
648
|
+
}
|
|
649
|
+
|
|
562
650
|
.file-viewer-editor-line-num-col {
|
|
563
651
|
width: 56px;
|
|
564
652
|
flex-shrink: 0;
|
|
@@ -975,17 +1063,110 @@
|
|
|
975
1063
|
color: var(--text-muted);
|
|
976
1064
|
}
|
|
977
1065
|
|
|
978
|
-
.sidebar-git-
|
|
1066
|
+
.sidebar-git-changes-label {
|
|
1067
|
+
padding: 7px 10px 4px;
|
|
1068
|
+
font-size: 10px;
|
|
1069
|
+
letter-spacing: 0.06em;
|
|
1070
|
+
text-transform: uppercase;
|
|
1071
|
+
color: var(--text-dim);
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
.sidebar-git-changes-list {
|
|
979
1075
|
list-style: none;
|
|
980
1076
|
display: flex;
|
|
981
1077
|
flex-direction: column;
|
|
982
|
-
gap:
|
|
983
|
-
padding: 6px
|
|
1078
|
+
gap: 2px;
|
|
1079
|
+
padding: 0 6px;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
.sidebar-git-change-row {
|
|
1083
|
+
display: flex;
|
|
1084
|
+
align-items: center;
|
|
1085
|
+
justify-content: space-between;
|
|
1086
|
+
gap: 8px;
|
|
1087
|
+
min-height: 22px;
|
|
1088
|
+
padding: 2px 6px;
|
|
1089
|
+
border-radius: 6px;
|
|
1090
|
+
line-height: 1.25;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
.sidebar-git-change-row.is-clickable {
|
|
1094
|
+
cursor: pointer;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
.sidebar-git-change-row.is-clickable:hover {
|
|
1098
|
+
background: rgba(255, 255, 255, 0.04);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
.sidebar-git-change-path {
|
|
1102
|
+
min-width: 0;
|
|
1103
|
+
white-space: nowrap;
|
|
1104
|
+
overflow: hidden;
|
|
1105
|
+
text-overflow: ellipsis;
|
|
1106
|
+
color: var(--text-muted);
|
|
1107
|
+
font-weight: 400;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
.sidebar-git-change-meta {
|
|
1111
|
+
display: inline-flex;
|
|
1112
|
+
align-items: center;
|
|
1113
|
+
gap: 6px;
|
|
1114
|
+
flex-shrink: 0;
|
|
1115
|
+
font-size: 10px;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
.sidebar-git-change-plus {
|
|
1119
|
+
color: #71f8a7;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
.sidebar-git-change-minus {
|
|
1123
|
+
color: #f3a86a;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.sidebar-git-change-status {
|
|
1127
|
+
font-size: 10px;
|
|
1128
|
+
letter-spacing: 0.06em;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
.sidebar-git-change-row.is-untracked .sidebar-git-change-status {
|
|
1132
|
+
color: #71f8a7;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
.sidebar-git-change-row.is-modified .sidebar-git-change-status {
|
|
1136
|
+
color: #63ebff;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
.sidebar-git-change-row.is-deleted .sidebar-git-change-status {
|
|
1140
|
+
color: #f87171;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
.sidebar-git-change-row.is-deleted .sidebar-git-change-path {
|
|
1144
|
+
text-decoration: line-through;
|
|
1145
|
+
text-decoration-thickness: 1px;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
.sidebar-git-scroll {
|
|
984
1149
|
overflow-y: auto;
|
|
985
1150
|
min-height: 0;
|
|
986
1151
|
flex: 1 1 auto;
|
|
987
1152
|
}
|
|
988
1153
|
|
|
1154
|
+
.sidebar-git-actions {
|
|
1155
|
+
padding: 8px 10px 6px;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
.sidebar-git-sync-button {
|
|
1159
|
+
width: 100%;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
.sidebar-git-list {
|
|
1163
|
+
list-style: none;
|
|
1164
|
+
display: flex;
|
|
1165
|
+
flex-direction: column;
|
|
1166
|
+
gap: 3px;
|
|
1167
|
+
padding: 6px 10px 0;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
989
1170
|
.sidebar-git-list li {
|
|
990
1171
|
display: flex;
|
|
991
1172
|
align-items: baseline;
|
package/lib/public/css/theme.css
CHANGED
package/lib/public/js/app.js
CHANGED
|
@@ -383,7 +383,9 @@ const App = () => {
|
|
|
383
383
|
const [acDismissed, setAcDismissed] = useState(false);
|
|
384
384
|
const [authEnabled, setAuthEnabled] = useState(false);
|
|
385
385
|
const [menuOpen, setMenuOpen] = useState(false);
|
|
386
|
-
const [sidebarTab, setSidebarTab] = useState(
|
|
386
|
+
const [sidebarTab, setSidebarTab] = useState(() =>
|
|
387
|
+
location.startsWith("/browse") ? "browse" : "menu",
|
|
388
|
+
);
|
|
387
389
|
const [sidebarWidthPx, setSidebarWidthPx] = useState(() => {
|
|
388
390
|
const settings = readUiSettings();
|
|
389
391
|
if (!Number.isFinite(settings.sidebarWidthPx)) return kDefaultSidebarWidthPx;
|
|
@@ -591,21 +593,24 @@ const App = () => {
|
|
|
591
593
|
`;
|
|
592
594
|
}
|
|
593
595
|
|
|
594
|
-
const buildBrowseRoute = (relativePath) => {
|
|
596
|
+
const buildBrowseRoute = (relativePath, options = {}) => {
|
|
597
|
+
const view = String(options?.view || "edit");
|
|
595
598
|
const encodedPath = String(relativePath || "")
|
|
596
599
|
.split("/")
|
|
597
600
|
.filter(Boolean)
|
|
598
601
|
.map((segment) => encodeURIComponent(segment))
|
|
599
602
|
.join("/");
|
|
600
|
-
|
|
603
|
+
const baseRoute = encodedPath ? `/browse/${encodedPath}` : "/browse";
|
|
604
|
+
if (view === "diff" && encodedPath) return `${baseRoute}?view=diff`;
|
|
605
|
+
return baseRoute;
|
|
601
606
|
};
|
|
602
607
|
const navigateToSubScreen = (screen) => {
|
|
603
608
|
setLocation(`/${screen}`);
|
|
604
609
|
setMobileSidebarOpen(false);
|
|
605
610
|
};
|
|
606
|
-
const navigateToBrowseFile = (relativePath) => {
|
|
611
|
+
const navigateToBrowseFile = (relativePath, options = {}) => {
|
|
607
612
|
setBrowsePreviewPath("");
|
|
608
|
-
setLocation(buildBrowseRoute(relativePath));
|
|
613
|
+
setLocation(buildBrowseRoute(relativePath, options));
|
|
609
614
|
setMobileSidebarOpen(false);
|
|
610
615
|
};
|
|
611
616
|
const handleSidebarLogout = async () => {
|
|
@@ -668,8 +673,13 @@ const App = () => {
|
|
|
668
673
|
];
|
|
669
674
|
|
|
670
675
|
const isBrowseRoute = location.startsWith("/browse");
|
|
676
|
+
const browseRoutePath = isBrowseRoute ? String(location || "").split("?")[0] : "";
|
|
677
|
+
const browseRouteQuery =
|
|
678
|
+
isBrowseRoute && String(location || "").includes("?")
|
|
679
|
+
? String(location || "").split("?").slice(1).join("?")
|
|
680
|
+
: "";
|
|
671
681
|
const selectedBrowsePath = isBrowseRoute
|
|
672
|
-
?
|
|
682
|
+
? browseRoutePath
|
|
673
683
|
.replace(/^\/browse\/?/, "")
|
|
674
684
|
.split("/")
|
|
675
685
|
.filter(Boolean)
|
|
@@ -682,6 +692,12 @@ const App = () => {
|
|
|
682
692
|
})
|
|
683
693
|
.join("/")
|
|
684
694
|
: "";
|
|
695
|
+
const activeBrowsePath = browsePreviewPath || selectedBrowsePath;
|
|
696
|
+
const browseViewerMode =
|
|
697
|
+
!browsePreviewPath &&
|
|
698
|
+
new URLSearchParams(browseRouteQuery).get("view") === "diff"
|
|
699
|
+
? "diff"
|
|
700
|
+
: "edit";
|
|
685
701
|
const selectedNavId = isBrowseRoute
|
|
686
702
|
? "browse"
|
|
687
703
|
: location === "/telegram"
|
|
@@ -727,6 +743,28 @@ const App = () => {
|
|
|
727
743
|
);
|
|
728
744
|
}, [isBrowseRoute, selectedBrowsePath]);
|
|
729
745
|
|
|
746
|
+
useEffect(() => {
|
|
747
|
+
const handleBrowseGitSynced = () => {
|
|
748
|
+
if (!isBrowseRoute || browseViewerMode !== "diff") return;
|
|
749
|
+
const activePath = String(selectedBrowsePath || "").trim();
|
|
750
|
+
if (!activePath) return;
|
|
751
|
+
setLocation(buildBrowseRoute(activePath, { view: "edit" }));
|
|
752
|
+
};
|
|
753
|
+
window.addEventListener("alphaclaw:browse-git-synced", handleBrowseGitSynced);
|
|
754
|
+
return () => {
|
|
755
|
+
window.removeEventListener(
|
|
756
|
+
"alphaclaw:browse-git-synced",
|
|
757
|
+
handleBrowseGitSynced,
|
|
758
|
+
);
|
|
759
|
+
};
|
|
760
|
+
}, [
|
|
761
|
+
isBrowseRoute,
|
|
762
|
+
browseViewerMode,
|
|
763
|
+
selectedBrowsePath,
|
|
764
|
+
setLocation,
|
|
765
|
+
buildBrowseRoute,
|
|
766
|
+
]);
|
|
767
|
+
|
|
730
768
|
useEffect(() => {
|
|
731
769
|
const settings = readUiSettings();
|
|
732
770
|
settings.sidebarWidthPx = sidebarWidthPx;
|
|
@@ -848,11 +886,23 @@ const App = () => {
|
|
|
848
886
|
${isBrowseRoute
|
|
849
887
|
? html`
|
|
850
888
|
<${FileViewer}
|
|
851
|
-
filePath=${
|
|
889
|
+
filePath=${activeBrowsePath}
|
|
852
890
|
isPreviewOnly=${Boolean(
|
|
853
891
|
browsePreviewPath &&
|
|
854
892
|
browsePreviewPath !== selectedBrowsePath,
|
|
855
893
|
)}
|
|
894
|
+
browseView=${browseViewerMode}
|
|
895
|
+
onRequestEdit=${(targetPath) => {
|
|
896
|
+
const normalizedTargetPath = String(targetPath || "");
|
|
897
|
+
if (
|
|
898
|
+
normalizedTargetPath &&
|
|
899
|
+
normalizedTargetPath !== selectedBrowsePath
|
|
900
|
+
) {
|
|
901
|
+
navigateToBrowseFile(normalizedTargetPath, { view: "edit" });
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
setLocation(buildBrowseRoute(selectedBrowsePath, { view: "edit" }));
|
|
905
|
+
}}
|
|
856
906
|
/>
|
|
857
907
|
`
|
|
858
908
|
: html`
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { h } from "https://esm.sh/preact";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from "https://esm.sh/preact/hooks";
|
|
3
9
|
import htm from "https://esm.sh/htm";
|
|
4
10
|
import { fetchBrowseTree } from "../lib/api.js";
|
|
5
11
|
import {
|
|
@@ -17,14 +23,45 @@ import {
|
|
|
17
23
|
FileCodeLineIcon,
|
|
18
24
|
Database2LineIcon,
|
|
19
25
|
HashtagIcon,
|
|
26
|
+
LockLineIcon,
|
|
20
27
|
} from "./icons.js";
|
|
28
|
+
import { LoadingSpinner } from "./loading-spinner.js";
|
|
21
29
|
|
|
22
30
|
const html = htm.bind(h);
|
|
23
31
|
const kTreeIndentPx = 9;
|
|
24
32
|
const kFolderBasePaddingPx = 10;
|
|
25
33
|
const kFileBasePaddingPx = 14;
|
|
34
|
+
const kTreeRefreshIntervalMs = 5000;
|
|
26
35
|
const kCollapsedFoldersStorageKey = "alphaclaw.browse.collapsedFolders";
|
|
27
36
|
const kLegacyCollapsedFoldersStorageKey = "alphaclawBrowseCollapsedFolders";
|
|
37
|
+
const kLockedBrowsePaths = new Set([
|
|
38
|
+
"hooks/bootstrap/agents.md",
|
|
39
|
+
"hooks/bootstrap/tools.md",
|
|
40
|
+
".alphaclaw/hourly-git-sync.sh",
|
|
41
|
+
".alphaclaw/.cli-device-auto-approved",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const normalizePolicyPath = (inputPath) =>
|
|
45
|
+
String(inputPath || "")
|
|
46
|
+
.replaceAll("\\", "/")
|
|
47
|
+
.replace(/^\.\/+/, "")
|
|
48
|
+
.replace(/^\/+/, "")
|
|
49
|
+
.trim()
|
|
50
|
+
.toLowerCase();
|
|
51
|
+
|
|
52
|
+
const matchesPolicyPath = (policyPathSet, normalizedPath) => {
|
|
53
|
+
const safeNormalizedPath = String(normalizedPath || "").trim();
|
|
54
|
+
if (!safeNormalizedPath) return false;
|
|
55
|
+
for (const policyPath of policyPathSet) {
|
|
56
|
+
if (
|
|
57
|
+
safeNormalizedPath === policyPath ||
|
|
58
|
+
safeNormalizedPath.endsWith(`/${policyPath}`)
|
|
59
|
+
) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
};
|
|
28
65
|
|
|
29
66
|
const readStoredCollapsedPaths = () => {
|
|
30
67
|
try {
|
|
@@ -165,6 +202,10 @@ const TreeNode = ({
|
|
|
165
202
|
const isActive = selectedPath === node.path;
|
|
166
203
|
const isSearchActiveNode = searchActivePath === node.path;
|
|
167
204
|
const hasDraft = draftPaths.has(node.path || "");
|
|
205
|
+
const isLocked = matchesPolicyPath(
|
|
206
|
+
kLockedBrowsePaths,
|
|
207
|
+
normalizePolicyPath(node.path || ""),
|
|
208
|
+
);
|
|
168
209
|
const fileIconMeta = getFileIconMeta(node.name);
|
|
169
210
|
const FileTypeIcon = fileIconMeta.icon;
|
|
170
211
|
return html`
|
|
@@ -179,7 +220,14 @@ const TreeNode = ({
|
|
|
179
220
|
>
|
|
180
221
|
<${FileTypeIcon} className=${fileIconMeta.className} />
|
|
181
222
|
<span class="tree-label">${node.name}</span>
|
|
182
|
-
${
|
|
223
|
+
${isLocked
|
|
224
|
+
? html`<${LockLineIcon}
|
|
225
|
+
className="tree-lock-icon"
|
|
226
|
+
title="Managed by Alpha Claw"
|
|
227
|
+
/>`
|
|
228
|
+
: hasDraft
|
|
229
|
+
? html`<span class="tree-draft-dot" aria-hidden="true"></span>`
|
|
230
|
+
: null}
|
|
183
231
|
</a>
|
|
184
232
|
</li>
|
|
185
233
|
`;
|
|
@@ -237,34 +285,55 @@ export const FileTree = ({
|
|
|
237
285
|
const [searchQuery, setSearchQuery] = useState("");
|
|
238
286
|
const [searchActivePath, setSearchActivePath] = useState("");
|
|
239
287
|
const searchInputRef = useRef(null);
|
|
288
|
+
const treeSignatureRef = useRef("");
|
|
240
289
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
290
|
+
const loadTree = useCallback(async ({ showLoading = false } = {}) => {
|
|
291
|
+
if (showLoading) setLoading(true);
|
|
292
|
+
if (showLoading) setError("");
|
|
293
|
+
try {
|
|
294
|
+
const data = await fetchBrowseTree();
|
|
295
|
+
const nextRoot = data.root || null;
|
|
296
|
+
const nextSignature = JSON.stringify(nextRoot || {});
|
|
297
|
+
if (treeSignatureRef.current !== nextSignature) {
|
|
298
|
+
treeSignatureRef.current = nextSignature;
|
|
299
|
+
setTreeRoot(nextRoot);
|
|
300
|
+
}
|
|
301
|
+
setCollapsedPaths((previousPaths) => {
|
|
302
|
+
if (previousPaths instanceof Set) return previousPaths;
|
|
303
|
+
const nextPaths = new Set();
|
|
304
|
+
collectFolderPaths(nextRoot, nextPaths);
|
|
305
|
+
return nextPaths;
|
|
306
|
+
});
|
|
307
|
+
if (showLoading) setError("");
|
|
308
|
+
} catch (loadError) {
|
|
309
|
+
if (showLoading) {
|
|
258
310
|
setError(loadError.message || "Could not load file tree");
|
|
259
|
-
} finally {
|
|
260
|
-
if (active) setLoading(false);
|
|
261
311
|
}
|
|
312
|
+
} finally {
|
|
313
|
+
if (showLoading) setLoading(false);
|
|
314
|
+
}
|
|
315
|
+
}, []);
|
|
316
|
+
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
loadTree({ showLoading: true });
|
|
319
|
+
}, [loadTree]);
|
|
320
|
+
|
|
321
|
+
useEffect(() => {
|
|
322
|
+
const refreshTree = () => {
|
|
323
|
+
loadTree({ showLoading: false });
|
|
262
324
|
};
|
|
263
|
-
|
|
325
|
+
const refreshInterval = window.setInterval(
|
|
326
|
+
refreshTree,
|
|
327
|
+
kTreeRefreshIntervalMs,
|
|
328
|
+
);
|
|
329
|
+
window.addEventListener("alphaclaw:browse-file-saved", refreshTree);
|
|
330
|
+
window.addEventListener("alphaclaw:browse-tree-refresh", refreshTree);
|
|
264
331
|
return () => {
|
|
265
|
-
|
|
332
|
+
window.clearInterval(refreshInterval);
|
|
333
|
+
window.removeEventListener("alphaclaw:browse-file-saved", refreshTree);
|
|
334
|
+
window.removeEventListener("alphaclaw:browse-tree-refresh", refreshTree);
|
|
266
335
|
};
|
|
267
|
-
}, []);
|
|
336
|
+
}, [loadTree]);
|
|
268
337
|
|
|
269
338
|
const normalizedSearchQuery = String(searchQuery || "").trim().toLowerCase();
|
|
270
339
|
const rootChildren = useMemo(() => {
|
|
@@ -430,7 +499,13 @@ export const FileTree = ({
|
|
|
430
499
|
};
|
|
431
500
|
|
|
432
501
|
if (loading) {
|
|
433
|
-
return html
|
|
502
|
+
return html`
|
|
503
|
+
<div class="file-tree-wrap file-tree-wrap-loading">
|
|
504
|
+
<div class="file-tree-state file-tree-state-loading">
|
|
505
|
+
<${LoadingSpinner} className="h-5 w-5 text-gray-400" />
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
`;
|
|
434
509
|
}
|
|
435
510
|
if (error) {
|
|
436
511
|
return html`<div class="file-tree-state file-tree-state-error">
|