@chrysb/alphaclaw 0.3.5-beta.0 → 0.3.5-beta.1

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 (44) hide show
  1. package/bin/alphaclaw.js +65 -1
  2. package/lib/public/css/explorer.css +201 -6
  3. package/lib/public/js/app.js +45 -1
  4. package/lib/public/js/components/channels.js +1 -0
  5. package/lib/public/js/components/file-tree.js +56 -67
  6. package/lib/public/js/components/file-viewer/constants.js +6 -0
  7. package/lib/public/js/components/file-viewer/diff-viewer.js +46 -0
  8. package/lib/public/js/components/file-viewer/editor-surface.js +120 -0
  9. package/lib/public/js/components/file-viewer/frontmatter-panel.js +56 -0
  10. package/lib/public/js/components/file-viewer/index.js +164 -0
  11. package/lib/public/js/components/file-viewer/markdown-split-view.js +51 -0
  12. package/lib/public/js/components/file-viewer/media-preview.js +44 -0
  13. package/lib/public/js/components/file-viewer/scroll-sync.js +95 -0
  14. package/lib/public/js/components/file-viewer/sqlite-viewer.js +167 -0
  15. package/lib/public/js/components/file-viewer/status-banners.js +59 -0
  16. package/lib/public/js/components/file-viewer/storage.js +58 -0
  17. package/lib/public/js/components/file-viewer/toolbar.js +77 -0
  18. package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +87 -0
  19. package/lib/public/js/components/file-viewer/use-file-diff.js +49 -0
  20. package/lib/public/js/components/file-viewer/use-file-loader.js +302 -0
  21. package/lib/public/js/components/file-viewer/use-file-viewer-draft-sync.js +32 -0
  22. package/lib/public/js/components/file-viewer/use-file-viewer-hotkeys.js +25 -0
  23. package/lib/public/js/components/file-viewer/use-file-viewer.js +379 -0
  24. package/lib/public/js/components/file-viewer/utils.js +11 -0
  25. package/lib/public/js/components/gateway.js +83 -30
  26. package/lib/public/js/components/icons.js +13 -0
  27. package/lib/public/js/components/sidebar-git-panel.js +72 -11
  28. package/lib/public/js/components/usage-tab.js +4 -1
  29. package/lib/public/js/components/watchdog-tab.js +6 -0
  30. package/lib/public/js/lib/api.js +16 -0
  31. package/lib/public/js/lib/browse-file-policies.js +34 -0
  32. package/lib/scripts/git +40 -0
  33. package/lib/scripts/git-askpass +6 -0
  34. package/lib/server/constants.js +8 -0
  35. package/lib/server/routes/browse/constants.js +51 -0
  36. package/lib/server/routes/browse/file-helpers.js +43 -0
  37. package/lib/server/routes/browse/git.js +131 -0
  38. package/lib/server/routes/{browse.js → browse/index.js} +290 -218
  39. package/lib/server/routes/browse/path-utils.js +53 -0
  40. package/lib/server/routes/browse/sqlite.js +140 -0
  41. package/lib/server/routes/proxy.js +11 -5
  42. package/lib/setup/core-prompts/TOOLS.md +0 -4
  43. package/package.json +1 -1
  44. package/lib/public/js/components/file-viewer.js +0 -1095
package/bin/alphaclaw.js CHANGED
@@ -753,6 +753,25 @@ if (fs.existsSync(configPath)) {
753
753
  stdio: "ignore",
754
754
  env: gitEnv,
755
755
  });
756
+ try {
757
+ execSync("git show-ref --verify --quiet refs/heads/main", {
758
+ cwd: openclawDir,
759
+ stdio: "ignore",
760
+ });
761
+ try {
762
+ execSync("git rev-parse --abbrev-ref --symbolic-full-name main@{upstream}", {
763
+ cwd: openclawDir,
764
+ stdio: "ignore",
765
+ });
766
+ } catch {
767
+ execSync("git branch --set-upstream-to=origin/main main", {
768
+ cwd: openclawDir,
769
+ stdio: "ignore",
770
+ env: gitEnv,
771
+ });
772
+ console.log("[alphaclaw] Set main upstream to origin/main");
773
+ }
774
+ } catch {}
756
775
  const remoteConfig = String(
757
776
  execSync(`git show "origin/${branch}:openclaw.json"`, {
758
777
  cwd: openclawDir,
@@ -871,7 +890,52 @@ try {
871
890
  }
872
891
 
873
892
  // ---------------------------------------------------------------------------
874
- // 13. Start Express server
893
+ // 13. Install git auth shim
894
+ // ---------------------------------------------------------------------------
895
+
896
+ try {
897
+ const gitAskPassSrc = path.join(__dirname, "..", "lib", "scripts", "git-askpass");
898
+ const gitAskPassDest = "/tmp/alphaclaw-git-askpass.sh";
899
+ const gitShimTemplatePath = path.join(__dirname, "..", "lib", "scripts", "git");
900
+ const gitShimDest = "/usr/local/bin/git";
901
+
902
+ if (fs.existsSync(gitAskPassSrc)) {
903
+ fs.copyFileSync(gitAskPassSrc, gitAskPassDest);
904
+ fs.chmodSync(gitAskPassDest, 0o755);
905
+ }
906
+
907
+ if (fs.existsSync(gitShimTemplatePath)) {
908
+ let realGitPath = "/usr/bin/git";
909
+ try {
910
+ const gitCandidates = String(
911
+ execSync("which -a git", {
912
+ stdio: ["ignore", "pipe", "ignore"],
913
+ encoding: "utf8",
914
+ }),
915
+ )
916
+ .split("\n")
917
+ .map((candidate) => candidate.trim())
918
+ .filter(Boolean);
919
+ const normalizedShimDest = path.resolve(gitShimDest);
920
+ const selectedCandidate = gitCandidates.find(
921
+ (candidatePath) => path.resolve(candidatePath) !== normalizedShimDest,
922
+ );
923
+ if (selectedCandidate) realGitPath = selectedCandidate;
924
+ } catch {}
925
+
926
+ const gitShimTemplate = fs.readFileSync(gitShimTemplatePath, "utf8");
927
+ const gitShimContent = gitShimTemplate
928
+ .replace("@@REAL_GIT@@", realGitPath)
929
+ .replace("@@OPENCLAW_REPO_ROOT@@", openclawDir);
930
+ fs.writeFileSync(gitShimDest, gitShimContent, { mode: 0o755 });
931
+ console.log("[alphaclaw] git auth shim installed");
932
+ }
933
+ } catch (e) {
934
+ console.log(`[alphaclaw] git auth shim skipped: ${e.message}`);
935
+ }
936
+
937
+ // ---------------------------------------------------------------------------
938
+ // 14. Start Express server
875
939
  // ---------------------------------------------------------------------------
876
940
 
877
941
  console.log("[alphaclaw] Setup complete -- starting server");
@@ -259,6 +259,10 @@
259
259
  color: #ff7ac6;
260
260
  }
261
261
 
262
+ .file-icon-audio {
263
+ color: #f5a6ff;
264
+ }
265
+
262
266
  .file-icon-shell {
263
267
  color: #71f8a7;
264
268
  }
@@ -929,6 +933,187 @@
929
933
  color: var(--text-muted);
930
934
  }
931
935
 
936
+ .file-viewer-image-shell {
937
+ flex: 1 1 auto;
938
+ min-height: 0;
939
+ height: 100%;
940
+ display: flex;
941
+ align-items: center;
942
+ justify-content: center;
943
+ padding: 18px;
944
+ overflow: auto;
945
+ }
946
+
947
+ .file-viewer-image {
948
+ max-width: 100%;
949
+ max-height: 100%;
950
+ width: auto;
951
+ height: auto;
952
+ border-radius: 8px;
953
+ border: 1px solid var(--border);
954
+ background: rgba(255, 255, 255, 0.02);
955
+ box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35);
956
+ }
957
+
958
+ .file-viewer-audio-shell {
959
+ flex: 1 1 auto;
960
+ min-height: 0;
961
+ height: 100%;
962
+ display: flex;
963
+ align-items: center;
964
+ justify-content: center;
965
+ padding: 18px;
966
+ overflow: auto;
967
+ }
968
+
969
+ .file-viewer-audio-player {
970
+ width: min(640px, 100%);
971
+ }
972
+
973
+ .file-viewer-sqlite-shell {
974
+ flex: 1 1 auto;
975
+ min-height: 0;
976
+ height: 100%;
977
+ overflow: auto;
978
+ padding: 14px 16px 22px;
979
+ }
980
+
981
+ .file-viewer-sqlite-header {
982
+ font-size: 12px;
983
+ color: var(--text-dim);
984
+ margin-bottom: 10px;
985
+ }
986
+
987
+ .file-viewer-sqlite-footer {
988
+ margin-top: 10px;
989
+ text-align: center;
990
+ font-size: 12px;
991
+ color: var(--text-dim);
992
+ }
993
+
994
+ .file-viewer-sqlite-list {
995
+ flex: 0 0 240px;
996
+ display: flex;
997
+ flex-direction: column;
998
+ gap: 8px;
999
+ }
1000
+
1001
+ .file-viewer-sqlite-layout {
1002
+ display: flex;
1003
+ gap: 12px;
1004
+ align-items: stretch;
1005
+ min-height: 0;
1006
+ }
1007
+
1008
+ .file-viewer-sqlite-card {
1009
+ width: 100%;
1010
+ text-align: left;
1011
+ cursor: pointer;
1012
+ display: flex;
1013
+ align-items: center;
1014
+ border: 1px solid var(--border);
1015
+ border-radius: 8px;
1016
+ padding: 7px 10px;
1017
+ background: rgba(255, 255, 255, 0.02);
1018
+ font: inherit;
1019
+ }
1020
+
1021
+ .file-viewer-sqlite-card:hover {
1022
+ background: rgba(255, 255, 255, 0.04);
1023
+ }
1024
+
1025
+ .file-viewer-sqlite-card.is-active {
1026
+ border-color: rgba(99, 235, 255, 0.45);
1027
+ background: rgba(99, 235, 255, 0.08);
1028
+ }
1029
+
1030
+ .file-viewer-sqlite-title {
1031
+ width: 100%;
1032
+ display: flex;
1033
+ align-items: center;
1034
+ justify-content: space-between;
1035
+ gap: 10px;
1036
+ font-size: 12px;
1037
+ color: var(--text);
1038
+ margin-bottom: 0;
1039
+ }
1040
+
1041
+ .file-viewer-sqlite-type {
1042
+ font-size: 10px;
1043
+ letter-spacing: 0.06em;
1044
+ text-transform: uppercase;
1045
+ color: var(--text-dim);
1046
+ }
1047
+
1048
+ .file-viewer-sqlite-table-shell {
1049
+ flex: 1;
1050
+ min-width: 0;
1051
+ border: 1px solid var(--border);
1052
+ border-radius: 8px;
1053
+ padding: 10px;
1054
+ background: rgba(255, 255, 255, 0.015);
1055
+ display: flex;
1056
+ flex-direction: column;
1057
+ min-height: 0;
1058
+ }
1059
+
1060
+ .file-viewer-sqlite-table-header {
1061
+ display: flex;
1062
+ align-items: center;
1063
+ justify-content: space-between;
1064
+ gap: 10px;
1065
+ }
1066
+
1067
+ .file-viewer-sqlite-table-name {
1068
+ font-size: 12px;
1069
+ color: var(--text);
1070
+ font-weight: 600;
1071
+ }
1072
+
1073
+ .file-viewer-sqlite-table-nav {
1074
+ display: inline-flex;
1075
+ gap: 6px;
1076
+ }
1077
+
1078
+ .file-viewer-sqlite-table-meta {
1079
+ margin-top: 4px;
1080
+ margin-bottom: 8px;
1081
+ font-size: 11px;
1082
+ color: var(--text-dim);
1083
+ }
1084
+
1085
+ .file-viewer-sqlite-table-wrap {
1086
+ min-height: 0;
1087
+ overflow: auto;
1088
+ }
1089
+
1090
+ .file-viewer-sqlite-table {
1091
+ width: 100%;
1092
+ border-collapse: collapse;
1093
+ table-layout: fixed;
1094
+ }
1095
+
1096
+ .file-viewer-sqlite-table th,
1097
+ .file-viewer-sqlite-table td {
1098
+ border: 1px solid var(--border);
1099
+ padding: 6px 8px;
1100
+ font-size: 11px;
1101
+ color: var(--text-muted);
1102
+ text-align: left;
1103
+ white-space: nowrap;
1104
+ overflow: hidden;
1105
+ text-overflow: ellipsis;
1106
+ }
1107
+
1108
+ .file-viewer-sqlite-table th {
1109
+ color: var(--text);
1110
+ background: rgba(255, 255, 255, 0.04);
1111
+ }
1112
+
1113
+ .file-viewer-sqlite-table-empty {
1114
+ color: var(--text-dim);
1115
+ }
1116
+
932
1117
  .file-viewer-state-error {
933
1118
  color: #f87171;
934
1119
  }
@@ -1045,20 +1230,30 @@
1045
1230
  text-overflow: ellipsis;
1046
1231
  }
1047
1232
 
1048
- .sidebar-git-dirty {
1049
- font-size: 10px;
1050
- text-transform: uppercase;
1051
- letter-spacing: 0.06em;
1233
+ .sidebar-git-sync-status {
1234
+ font-size: 12px;
1235
+ font-weight: 600;
1236
+ line-height: 1;
1052
1237
  }
1053
1238
 
1054
- .sidebar-git-dirty.is-clean {
1239
+ .sidebar-git-sync-status.is-up-to-date {
1055
1240
  color: #71f8a7;
1056
1241
  }
1057
1242
 
1058
- .sidebar-git-dirty.is-dirty {
1243
+ .sidebar-git-sync-status.is-ahead,
1244
+ .sidebar-git-sync-status.is-diverged {
1059
1245
  color: #f3a86a;
1060
1246
  }
1061
1247
 
1248
+ .sidebar-git-sync-status.is-behind {
1249
+ color: #93c5fd;
1250
+ }
1251
+
1252
+ .sidebar-git-sync-status.is-no-upstream,
1253
+ .sidebar-git-sync-status.is-upstream-gone {
1254
+ color: var(--text-dim);
1255
+ }
1256
+
1062
1257
  .sidebar-git-meta {
1063
1258
  color: var(--text-muted);
1064
1259
  }
@@ -21,6 +21,7 @@ import {
21
21
  restartGateway,
22
22
  fetchWatchdogStatus,
23
23
  triggerWatchdogRepair,
24
+ updateOpenclaw,
24
25
  } from "./lib/api.js";
25
26
  import { usePolling } from "./hooks/usePolling.js";
26
27
  import { Gateway } from "./components/gateway.js";
@@ -40,7 +41,7 @@ import { UpdateActionButton } from "./components/update-action-button.js";
40
41
  import { GlobalRestartBanner } from "./components/global-restart-banner.js";
41
42
  import { LoadingSpinner } from "./components/loading-spinner.js";
42
43
  import { WatchdogTab } from "./components/watchdog-tab.js";
43
- import { FileViewer } from "./components/file-viewer.js";
44
+ import { FileViewer } from "./components/file-viewer/index.js";
44
45
  import { AppSidebar } from "./components/sidebar.js";
45
46
  import { UsageTab } from "./components/usage-tab.js";
46
47
  import { readUiSettings, writeUiSettings } from "./lib/ui-settings.js";
@@ -101,6 +102,9 @@ const GeneralTab = ({
101
102
  restartingGateway,
102
103
  onRestartGateway,
103
104
  restartSignal = 0,
105
+ openclawUpdateInProgress = false,
106
+ onOpenclawVersionActionComplete = () => {},
107
+ onOpenclawUpdate,
104
108
  }) => {
105
109
  const [googleKey, setGoogleKey] = useState(0);
106
110
  const [dashboardLoading, setDashboardLoading] = useState(false);
@@ -263,6 +267,9 @@ const GeneralTab = ({
263
267
  onOpenWatchdog=${() => onSwitchTab("watchdog")}
264
268
  onRepair=${handleWatchdogRepair}
265
269
  repairing=${repairingWatchdog}
270
+ openclawUpdateInProgress=${openclawUpdateInProgress}
271
+ onOpenclawVersionActionComplete=${onOpenclawVersionActionComplete}
272
+ onOpenclawUpdate=${onOpenclawUpdate}
266
273
  />
267
274
  <${Channels} channels=${channels} onSwitchTab=${onSwitchTab} onNavigate=${onNavigate} />
268
275
  <${Pairings}
@@ -417,6 +424,7 @@ const App = () => {
417
424
  const [restartingGateway, setRestartingGateway] = useState(false);
418
425
  const [gatewayRestartSignal, setGatewayRestartSignal] = useState(0);
419
426
  const [statusPollCadenceMs, setStatusPollCadenceMs] = useState(15000);
427
+ const [openclawUpdateInProgress, setOpenclawUpdateInProgress] = useState(false);
420
428
  const menuRef = useRef(null);
421
429
  const sharedStatusPoll = usePolling(fetchStatus, statusPollCadenceMs, {
422
430
  enabled: onboarded === true,
@@ -548,6 +556,36 @@ const App = () => {
548
556
  }
549
557
  }, [restartingGateway, refreshRestartStatus, refreshSharedStatuses]);
550
558
 
559
+ const handleOpenclawUpdate = useCallback(async () => {
560
+ if (openclawUpdateInProgress) {
561
+ return { ok: false, error: "OpenClaw update already in progress" };
562
+ }
563
+ setOpenclawUpdateInProgress(true);
564
+ try {
565
+ const data = await updateOpenclaw();
566
+ return data;
567
+ } finally {
568
+ setOpenclawUpdateInProgress(false);
569
+ refreshSharedStatuses();
570
+ setTimeout(refreshSharedStatuses, 1200);
571
+ setTimeout(refreshSharedStatuses, 3500);
572
+ setTimeout(refreshRestartStatus, 1200);
573
+ }
574
+ }, [
575
+ openclawUpdateInProgress,
576
+ refreshRestartStatus,
577
+ refreshSharedStatuses,
578
+ ]);
579
+
580
+ const handleOpenclawVersionActionComplete = useCallback(
581
+ ({ type }) => {
582
+ if (type !== "update") return;
583
+ refreshSharedStatuses();
584
+ setTimeout(refreshSharedStatuses, 1200);
585
+ },
586
+ [refreshSharedStatuses],
587
+ );
588
+
551
589
  const handleAcUpdate = async () => {
552
590
  if (acUpdating) return;
553
591
  setAcUpdating(true);
@@ -924,6 +962,9 @@ const App = () => {
924
962
  restartingGateway=${restartingGateway}
925
963
  onRestartGateway=${handleGatewayRestart}
926
964
  restartSignal=${gatewayRestartSignal}
965
+ openclawUpdateInProgress=${openclawUpdateInProgress}
966
+ onOpenclawVersionActionComplete=${handleOpenclawVersionActionComplete}
967
+ onOpenclawUpdate=${handleOpenclawUpdate}
927
968
  />
928
969
  </div>
929
970
  </${Route}>
@@ -942,6 +983,9 @@ const App = () => {
942
983
  restartingGateway=${restartingGateway}
943
984
  onRestartGateway=${handleGatewayRestart}
944
985
  restartSignal=${gatewayRestartSignal}
986
+ openclawUpdateInProgress=${openclawUpdateInProgress}
987
+ onOpenclawVersionActionComplete=${handleOpenclawVersionActionComplete}
988
+ onOpenclawUpdate=${handleOpenclawUpdate}
945
989
  />
946
990
  </div>
947
991
  </${Route}>
@@ -43,6 +43,7 @@ export function Channels({ channels, onSwitchTab, onNavigate }) {
43
43
  : null}
44
44
  ${channelMeta.label}
45
45
  ${isClickable && html`
46
+ <span class="text-xs text-gray-500 ml-1">Workspace</span>
46
47
  <svg width="14" height="14" viewBox="0 0 16 16" fill="none" class="text-gray-600">
47
48
  <path
48
49
  d="M6 3.5L10.5 8L6 12.5"
@@ -12,11 +12,17 @@ import {
12
12
  kDraftIndexChangedEventName,
13
13
  readStoredDraftPaths,
14
14
  } from "../lib/browse-draft-state.js";
15
+ import {
16
+ kLockedBrowsePaths,
17
+ matchesBrowsePolicyPath,
18
+ normalizeBrowsePolicyPath,
19
+ } from "../lib/browse-file-policies.js";
15
20
  import { collectAncestorFolderPaths } from "../lib/file-tree-utils.js";
16
21
  import {
17
22
  MarkdownFillIcon,
18
23
  JavascriptFillIcon,
19
24
  File3LineIcon,
25
+ FileMusicLineIcon,
20
26
  Image2FillIcon,
21
27
  TerminalFillIcon,
22
28
  BracesLineIcon,
@@ -32,42 +38,11 @@ const kTreeIndentPx = 9;
32
38
  const kFolderBasePaddingPx = 10;
33
39
  const kFileBasePaddingPx = 14;
34
40
  const kTreeRefreshIntervalMs = 5000;
35
- const kCollapsedFoldersStorageKey = "alphaclaw.browse.collapsedFolders";
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();
41
+ const kExpandedFoldersStorageKey = "alphaclaw.browse.expandedFolders";
51
42
 
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
- };
65
-
66
- const readStoredCollapsedPaths = () => {
43
+ const readStoredExpandedPaths = () => {
67
44
  try {
68
- const rawValue =
69
- window.localStorage.getItem(kCollapsedFoldersStorageKey) ||
70
- window.localStorage.getItem(kLegacyCollapsedFoldersStorageKey);
45
+ const rawValue = window.localStorage.getItem(kExpandedFoldersStorageKey);
71
46
  if (!rawValue) return null;
72
47
  const parsedValue = JSON.parse(rawValue);
73
48
  if (!Array.isArray(parsedValue)) return null;
@@ -98,7 +73,9 @@ const collectFilePaths = (node, filePaths) => {
98
73
 
99
74
  const filterTreeNode = (node, normalizedQuery) => {
100
75
  if (!node) return null;
101
- const query = String(normalizedQuery || "").trim().toLowerCase();
76
+ const query = String(normalizedQuery || "")
77
+ .trim()
78
+ .toLowerCase();
102
79
  if (!query) return node;
103
80
  const nodeName = String(node.name || "").toLowerCase();
104
81
  const nodePath = String(node.path || "").toLowerCase();
@@ -154,6 +131,12 @@ const getFileIconMeta = (fileName) => {
154
131
  className: "file-icon file-icon-image",
155
132
  };
156
133
  }
134
+ if (/\.(mp3|wav|ogg|oga|m4a|aac|flac|opus|weba)$/i.test(normalizedName)) {
135
+ return {
136
+ icon: FileMusicLineIcon,
137
+ className: "file-icon file-icon-audio",
138
+ };
139
+ }
157
140
  if (
158
141
  /\.(sh|bash|zsh|command)$/i.test(normalizedName) ||
159
142
  [
@@ -189,7 +172,7 @@ const getFileIconMeta = (fileName) => {
189
172
  const TreeNode = ({
190
173
  node,
191
174
  depth = 0,
192
- collapsedPaths,
175
+ expandedPaths,
193
176
  onToggleFolder,
194
177
  onSelectFile,
195
178
  selectedPath = "",
@@ -202,9 +185,9 @@ const TreeNode = ({
202
185
  const isActive = selectedPath === node.path;
203
186
  const isSearchActiveNode = searchActivePath === node.path;
204
187
  const hasDraft = draftPaths.has(node.path || "");
205
- const isLocked = matchesPolicyPath(
188
+ const isLocked = matchesBrowsePolicyPath(
206
189
  kLockedBrowsePaths,
207
- normalizePolicyPath(node.path || ""),
190
+ normalizeBrowsePolicyPath(node.path || ""),
208
191
  );
209
192
  const fileIconMeta = getFileIconMeta(node.name);
210
193
  const FileTypeIcon = fileIconMeta.icon;
@@ -223,7 +206,7 @@ const TreeNode = ({
223
206
  ${isLocked
224
207
  ? html`<${LockLineIcon}
225
208
  className="tree-lock-icon"
226
- title="Managed by Alpha Claw"
209
+ title="Managed by AlphaClaw"
227
210
  />`
228
211
  : hasDraft
229
212
  ? html`<span class="tree-draft-dot" aria-hidden="true"></span>`
@@ -234,7 +217,7 @@ const TreeNode = ({
234
217
  }
235
218
 
236
219
  const folderPath = node.path || "";
237
- const isCollapsed = isSearchActive ? false : collapsedPaths.has(folderPath);
220
+ const isCollapsed = isSearchActive ? false : !expandedPaths.has(folderPath);
238
221
  return html`
239
222
  <li class="tree-item">
240
223
  <div
@@ -255,7 +238,7 @@ const TreeNode = ({
255
238
  key=${childNode.path || `${folderPath}/${childNode.name}`}
256
239
  node=${childNode}
257
240
  depth=${depth + 1}
258
- collapsedPaths=${collapsedPaths}
241
+ expandedPaths=${expandedPaths}
259
242
  onToggleFolder=${onToggleFolder}
260
243
  onSelectFile=${onSelectFile}
261
244
  selectedPath=${selectedPath}
@@ -278,9 +261,7 @@ export const FileTree = ({
278
261
  const [treeRoot, setTreeRoot] = useState(null);
279
262
  const [loading, setLoading] = useState(true);
280
263
  const [error, setError] = useState("");
281
- const [collapsedPaths, setCollapsedPaths] = useState(
282
- readStoredCollapsedPaths,
283
- );
264
+ const [expandedPaths, setExpandedPaths] = useState(readStoredExpandedPaths);
284
265
  const [draftPaths, setDraftPaths] = useState(readStoredDraftPaths);
285
266
  const [searchQuery, setSearchQuery] = useState("");
286
267
  const [searchActivePath, setSearchActivePath] = useState("");
@@ -298,12 +279,9 @@ export const FileTree = ({
298
279
  treeSignatureRef.current = nextSignature;
299
280
  setTreeRoot(nextRoot);
300
281
  }
301
- setCollapsedPaths((previousPaths) => {
302
- if (previousPaths instanceof Set) return previousPaths;
303
- const nextPaths = new Set();
304
- collectFolderPaths(nextRoot, nextPaths);
305
- return nextPaths;
306
- });
282
+ setExpandedPaths((previousPaths) =>
283
+ previousPaths instanceof Set ? previousPaths : new Set(),
284
+ );
307
285
  if (showLoading) setError("");
308
286
  } catch (loadError) {
309
287
  if (showLoading) {
@@ -335,7 +313,9 @@ export const FileTree = ({
335
313
  };
336
314
  }, [loadTree]);
337
315
 
338
- const normalizedSearchQuery = String(searchQuery || "").trim().toLowerCase();
316
+ const normalizedSearchQuery = String(searchQuery || "")
317
+ .trim()
318
+ .toLowerCase();
339
319
  const rootChildren = useMemo(() => {
340
320
  const children = treeRoot?.children || [];
341
321
  if (!normalizedSearchQuery) return children;
@@ -343,8 +323,8 @@ export const FileTree = ({
343
323
  .map((node) => filterTreeNode(node, normalizedSearchQuery))
344
324
  .filter(Boolean);
345
325
  }, [treeRoot, normalizedSearchQuery]);
346
- const safeCollapsedPaths =
347
- collapsedPaths instanceof Set ? collapsedPaths : new Set();
326
+ const safeExpandedPaths =
327
+ expandedPaths instanceof Set ? expandedPaths : new Set();
348
328
  const isSearchActive = normalizedSearchQuery.length > 0;
349
329
  const filteredFilePaths = useMemo(() => {
350
330
  const filePaths = [];
@@ -353,26 +333,26 @@ export const FileTree = ({
353
333
  }, [rootChildren]);
354
334
 
355
335
  useEffect(() => {
356
- if (!(collapsedPaths instanceof Set)) return;
336
+ if (!(expandedPaths instanceof Set)) return;
357
337
  try {
358
338
  window.localStorage.setItem(
359
- kCollapsedFoldersStorageKey,
360
- JSON.stringify(Array.from(collapsedPaths)),
339
+ kExpandedFoldersStorageKey,
340
+ JSON.stringify(Array.from(expandedPaths)),
361
341
  );
362
342
  } catch {}
363
- }, [collapsedPaths]);
343
+ }, [expandedPaths]);
364
344
 
365
345
  useEffect(() => {
366
346
  if (!selectedPath) return;
367
347
  const ancestorFolderPaths = collectAncestorFolderPaths(selectedPath);
368
348
  if (!ancestorFolderPaths.length) return;
369
- setCollapsedPaths((previousPaths) => {
349
+ setExpandedPaths((previousPaths) => {
370
350
  if (!(previousPaths instanceof Set)) return previousPaths;
371
351
  let didChange = false;
372
352
  const nextPaths = new Set(previousPaths);
373
353
  ancestorFolderPaths.forEach((ancestorPath) => {
374
- if (nextPaths.has(ancestorPath)) {
375
- nextPaths.delete(ancestorPath);
354
+ if (!nextPaths.has(ancestorPath)) {
355
+ nextPaths.add(ancestorPath);
376
356
  didChange = true;
377
357
  }
378
358
  });
@@ -395,10 +375,16 @@ export const FileTree = ({
395
375
  }
396
376
  setDraftPaths(readStoredDraftPaths());
397
377
  };
398
- window.addEventListener(kDraftIndexChangedEventName, handleDraftIndexChanged);
378
+ window.addEventListener(
379
+ kDraftIndexChangedEventName,
380
+ handleDraftIndexChanged,
381
+ );
399
382
  window.addEventListener("storage", handleDraftIndexChanged);
400
383
  return () => {
401
- window.removeEventListener(kDraftIndexChangedEventName, handleDraftIndexChanged);
384
+ window.removeEventListener(
385
+ kDraftIndexChangedEventName,
386
+ handleDraftIndexChanged,
387
+ );
402
388
  window.removeEventListener("storage", handleDraftIndexChanged);
403
389
  };
404
390
  }, []);
@@ -431,13 +417,14 @@ export const FileTree = ({
431
417
  onPreviewFile("");
432
418
  return;
433
419
  }
434
- if (searchActivePath && filteredFilePaths.includes(searchActivePath)) return;
420
+ if (searchActivePath && filteredFilePaths.includes(searchActivePath))
421
+ return;
435
422
  setSearchActivePath("");
436
423
  onPreviewFile("");
437
424
  }, [isSearchActive, filteredFilePaths, searchActivePath, onPreviewFile]);
438
425
 
439
426
  const toggleFolder = (folderPath) => {
440
- setCollapsedPaths((previousPaths) => {
427
+ setExpandedPaths((previousPaths) => {
441
428
  const nextPaths =
442
429
  previousPaths instanceof Set ? new Set(previousPaths) : new Set();
443
430
  if (nextPaths.has(folderPath)) nextPaths.delete(folderPath);
@@ -460,7 +447,8 @@ export const FileTree = ({
460
447
  if (!filteredFilePaths.length) return;
461
448
  const currentIndex = filteredFilePaths.indexOf(searchActivePath);
462
449
  const delta = direction === "up" ? -1 : 1;
463
- const baseIndex = currentIndex === -1 ? (direction === "up" ? 0 : -1) : currentIndex;
450
+ const baseIndex =
451
+ currentIndex === -1 ? (direction === "up" ? 0 : -1) : currentIndex;
464
452
  const nextIndex =
465
453
  (baseIndex + delta + filteredFilePaths.length) % filteredFilePaths.length;
466
454
  const nextPath = filteredFilePaths[nextIndex];
@@ -470,7 +458,8 @@ export const FileTree = ({
470
458
 
471
459
  const commitSearchSelection = () => {
472
460
  const [singlePath = ""] = filteredFilePaths;
473
- const targetPath = searchActivePath || (filteredFilePaths.length === 1 ? singlePath : "");
461
+ const targetPath =
462
+ searchActivePath || (filteredFilePaths.length === 1 ? singlePath : "");
474
463
  if (!targetPath) return;
475
464
  onSelectFile(targetPath);
476
465
  clearSearch();
@@ -556,7 +545,7 @@ export const FileTree = ({
556
545
  <${TreeNode}
557
546
  key=${node.path || node.name}
558
547
  node=${node}
559
- collapsedPaths=${safeCollapsedPaths}
548
+ expandedPaths=${safeExpandedPaths}
560
549
  onToggleFolder=${toggleFolder}
561
550
  onSelectFile=${onSelectFile}
562
551
  selectedPath=${selectedPath}