@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.
- package/bin/alphaclaw.js +65 -1
- package/lib/public/css/explorer.css +201 -6
- package/lib/public/js/app.js +45 -1
- package/lib/public/js/components/channels.js +1 -0
- package/lib/public/js/components/file-tree.js +56 -67
- package/lib/public/js/components/file-viewer/constants.js +6 -0
- package/lib/public/js/components/file-viewer/diff-viewer.js +46 -0
- package/lib/public/js/components/file-viewer/editor-surface.js +120 -0
- package/lib/public/js/components/file-viewer/frontmatter-panel.js +56 -0
- package/lib/public/js/components/file-viewer/index.js +164 -0
- package/lib/public/js/components/file-viewer/markdown-split-view.js +51 -0
- package/lib/public/js/components/file-viewer/media-preview.js +44 -0
- package/lib/public/js/components/file-viewer/scroll-sync.js +95 -0
- package/lib/public/js/components/file-viewer/sqlite-viewer.js +167 -0
- package/lib/public/js/components/file-viewer/status-banners.js +59 -0
- package/lib/public/js/components/file-viewer/storage.js +58 -0
- package/lib/public/js/components/file-viewer/toolbar.js +77 -0
- package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +87 -0
- package/lib/public/js/components/file-viewer/use-file-diff.js +49 -0
- package/lib/public/js/components/file-viewer/use-file-loader.js +302 -0
- package/lib/public/js/components/file-viewer/use-file-viewer-draft-sync.js +32 -0
- package/lib/public/js/components/file-viewer/use-file-viewer-hotkeys.js +25 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +379 -0
- package/lib/public/js/components/file-viewer/utils.js +11 -0
- package/lib/public/js/components/gateway.js +83 -30
- package/lib/public/js/components/icons.js +13 -0
- package/lib/public/js/components/sidebar-git-panel.js +72 -11
- package/lib/public/js/components/usage-tab.js +4 -1
- package/lib/public/js/components/watchdog-tab.js +6 -0
- package/lib/public/js/lib/api.js +16 -0
- package/lib/public/js/lib/browse-file-policies.js +34 -0
- package/lib/scripts/git +40 -0
- package/lib/scripts/git-askpass +6 -0
- package/lib/server/constants.js +8 -0
- package/lib/server/routes/browse/constants.js +51 -0
- package/lib/server/routes/browse/file-helpers.js +43 -0
- package/lib/server/routes/browse/git.js +131 -0
- package/lib/server/routes/{browse.js → browse/index.js} +290 -218
- package/lib/server/routes/browse/path-utils.js +53 -0
- package/lib/server/routes/browse/sqlite.js +140 -0
- package/lib/server/routes/proxy.js +11 -5
- package/lib/setup/core-prompts/TOOLS.md +0 -4
- package/package.json +1 -1
- 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.
|
|
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-
|
|
1049
|
-
font-size:
|
|
1050
|
-
|
|
1051
|
-
|
|
1233
|
+
.sidebar-git-sync-status {
|
|
1234
|
+
font-size: 12px;
|
|
1235
|
+
font-weight: 600;
|
|
1236
|
+
line-height: 1;
|
|
1052
1237
|
}
|
|
1053
1238
|
|
|
1054
|
-
.sidebar-git-
|
|
1239
|
+
.sidebar-git-sync-status.is-up-to-date {
|
|
1055
1240
|
color: #71f8a7;
|
|
1056
1241
|
}
|
|
1057
1242
|
|
|
1058
|
-
.sidebar-git-
|
|
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
|
}
|
package/lib/public/js/app.js
CHANGED
|
@@ -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
|
|
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
|
|
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 || "")
|
|
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
|
-
|
|
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 =
|
|
188
|
+
const isLocked = matchesBrowsePolicyPath(
|
|
206
189
|
kLockedBrowsePaths,
|
|
207
|
-
|
|
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
|
|
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 :
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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 || "")
|
|
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
|
|
347
|
-
|
|
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 (!(
|
|
336
|
+
if (!(expandedPaths instanceof Set)) return;
|
|
357
337
|
try {
|
|
358
338
|
window.localStorage.setItem(
|
|
359
|
-
|
|
360
|
-
JSON.stringify(Array.from(
|
|
339
|
+
kExpandedFoldersStorageKey,
|
|
340
|
+
JSON.stringify(Array.from(expandedPaths)),
|
|
361
341
|
);
|
|
362
342
|
} catch {}
|
|
363
|
-
}, [
|
|
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
|
-
|
|
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.
|
|
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(
|
|
378
|
+
window.addEventListener(
|
|
379
|
+
kDraftIndexChangedEventName,
|
|
380
|
+
handleDraftIndexChanged,
|
|
381
|
+
);
|
|
399
382
|
window.addEventListener("storage", handleDraftIndexChanged);
|
|
400
383
|
return () => {
|
|
401
|
-
window.removeEventListener(
|
|
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))
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
548
|
+
expandedPaths=${safeExpandedPaths}
|
|
560
549
|
onToggleFolder=${toggleFolder}
|
|
561
550
|
onSelectFile=${onSelectFile}
|
|
562
551
|
selectedPath=${selectedPath}
|