@chrysb/alphaclaw 0.3.5-beta.1 → 0.4.1-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 +1 -31
- package/lib/public/assets/icons/google_icon.svg +8 -0
- package/lib/public/css/explorer.css +53 -0
- package/lib/public/css/shell.css +21 -19
- package/lib/public/css/theme.css +17 -0
- package/lib/public/js/app.js +205 -109
- package/lib/public/js/components/credentials-modal.js +36 -8
- package/lib/public/js/components/file-tree.js +212 -22
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
- package/lib/public/js/components/file-viewer/index.js +47 -6
- package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
- package/lib/public/js/components/file-viewer/status-banners.js +11 -6
- package/lib/public/js/components/file-viewer/toolbar.js +56 -1
- package/lib/public/js/components/file-viewer/use-editor-selection-restore.js +6 -0
- package/lib/public/js/components/file-viewer/use-file-diff.js +11 -0
- package/lib/public/js/components/file-viewer/use-file-loader.js +12 -2
- package/lib/public/js/components/file-viewer/use-file-viewer.js +142 -15
- package/lib/public/js/components/google/account-row.js +131 -0
- package/lib/public/js/components/google/add-account-modal.js +93 -0
- package/lib/public/js/components/google/gmail-setup-wizard.js +450 -0
- package/lib/public/js/components/google/gmail-watch-toggle.js +81 -0
- package/lib/public/js/components/google/index.js +553 -0
- package/lib/public/js/components/google/use-gmail-watch.js +140 -0
- package/lib/public/js/components/google/use-google-accounts.js +41 -0
- package/lib/public/js/components/icons.js +26 -0
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/sidebar-git-panel.js +48 -20
- package/lib/public/js/components/sidebar.js +93 -75
- package/lib/public/js/components/toast.js +11 -7
- package/lib/public/js/components/usage-tab/constants.js +31 -0
- package/lib/public/js/components/usage-tab/formatters.js +24 -0
- package/lib/public/js/components/usage-tab/index.js +72 -0
- package/lib/public/js/components/usage-tab/overview-section.js +147 -0
- package/lib/public/js/components/usage-tab/sessions-section.js +175 -0
- package/lib/public/js/components/usage-tab/use-usage-tab.js +241 -0
- package/lib/public/js/components/webhooks.js +182 -129
- package/lib/public/js/lib/api.js +178 -9
- package/lib/public/js/lib/browse-file-policies.js +29 -11
- package/lib/public/js/lib/format.js +71 -0
- package/lib/public/js/lib/syntax-highlighters/index.js +6 -5
- package/lib/public/shared/browse-file-policies.json +13 -0
- package/lib/server/constants.js +47 -7
- package/lib/server/gmail-push.js +109 -0
- package/lib/server/gmail-serve.js +254 -0
- package/lib/server/gmail-watch.js +725 -0
- package/lib/server/google-state.js +317 -0
- package/lib/server/helpers.js +17 -11
- package/lib/server/internal-files-migration.js +31 -3
- package/lib/server/onboarding/github.js +21 -2
- package/lib/server/onboarding/index.js +1 -3
- package/lib/server/onboarding/openclaw.js +3 -0
- package/lib/server/onboarding/workspace.js +40 -0
- package/lib/server/routes/browse/index.js +90 -2
- package/lib/server/routes/gmail.js +128 -0
- package/lib/server/routes/google.js +433 -213
- package/lib/server/routes/system.js +107 -0
- package/lib/server/routes/usage.js +29 -2
- package/lib/server/routes/webhooks.js +52 -17
- package/lib/server/usage-db.js +283 -15
- package/lib/server/watchdog.js +66 -0
- package/lib/server/webhook-middleware.js +99 -1
- package/lib/server/webhooks.js +214 -65
- package/lib/server.js +27 -0
- package/lib/setup/gitignore +6 -0
- package/lib/setup/hourly-git-sync.sh +29 -2
- package/package.json +1 -1
- package/lib/public/js/components/google.js +0 -228
- package/lib/public/js/components/usage-tab.js +0 -531
|
@@ -18,6 +18,7 @@ export const MarkdownSplitView = ({
|
|
|
18
18
|
editorTextareaRef,
|
|
19
19
|
renderContent,
|
|
20
20
|
handleContentInput,
|
|
21
|
+
handleEditorKeyDown,
|
|
21
22
|
handleEditorScroll,
|
|
22
23
|
handleEditorSelectionChange,
|
|
23
24
|
isEditBlocked,
|
|
@@ -43,6 +44,7 @@ export const MarkdownSplitView = ({
|
|
|
43
44
|
editorTextareaRef=${editorTextareaRef}
|
|
44
45
|
renderContent=${renderContent}
|
|
45
46
|
handleContentInput=${handleContentInput}
|
|
47
|
+
handleEditorKeyDown=${handleEditorKeyDown}
|
|
46
48
|
handleEditorScroll=${handleEditorScroll}
|
|
47
49
|
handleEditorSelectionChange=${handleEditorSelectionChange}
|
|
48
50
|
isEditBlocked=${isEditBlocked}
|
|
@@ -9,6 +9,7 @@ export const FileViewerStatusBanners = ({
|
|
|
9
9
|
isDiffView,
|
|
10
10
|
onRequestEdit,
|
|
11
11
|
normalizedPath,
|
|
12
|
+
isDeletedDiff = false,
|
|
12
13
|
isLockedFile,
|
|
13
14
|
isProtectedFile,
|
|
14
15
|
isProtectedLocked,
|
|
@@ -18,12 +19,16 @@ export const FileViewerStatusBanners = ({
|
|
|
18
19
|
? html`
|
|
19
20
|
<div class="file-viewer-protected-banner file-viewer-diff-banner">
|
|
20
21
|
<div class="file-viewer-protected-banner-text">Viewing unsynced changes</div>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
${!isDeletedDiff
|
|
23
|
+
? html`
|
|
24
|
+
<${ActionButton}
|
|
25
|
+
onClick=${() => onRequestEdit(normalizedPath)}
|
|
26
|
+
tone="secondary"
|
|
27
|
+
size="sm"
|
|
28
|
+
idleLabel="View file"
|
|
29
|
+
/>
|
|
30
|
+
`
|
|
31
|
+
: null}
|
|
27
32
|
</div>
|
|
28
33
|
`
|
|
29
34
|
: null}
|
|
@@ -2,7 +2,7 @@ import { h } from "https://esm.sh/preact";
|
|
|
2
2
|
import htm from "https://esm.sh/htm";
|
|
3
3
|
import { ActionButton } from "../action-button.js";
|
|
4
4
|
import { SegmentedControl } from "../segmented-control.js";
|
|
5
|
-
import { SaveFillIcon } from "../icons.js";
|
|
5
|
+
import { DeleteBinLineIcon, RestartLineIcon, SaveFillIcon } from "../icons.js";
|
|
6
6
|
|
|
7
7
|
const html = htm.bind(h);
|
|
8
8
|
|
|
@@ -15,6 +15,7 @@ export const FileViewerToolbar = ({
|
|
|
15
15
|
viewMode,
|
|
16
16
|
handleChangeViewMode,
|
|
17
17
|
handleSave,
|
|
18
|
+
handleDiscard,
|
|
18
19
|
loading,
|
|
19
20
|
canEditFile,
|
|
20
21
|
isEditBlocked,
|
|
@@ -22,6 +23,14 @@ export const FileViewerToolbar = ({
|
|
|
22
23
|
isAudioFile,
|
|
23
24
|
isSqliteFile,
|
|
24
25
|
saving,
|
|
26
|
+
deleting,
|
|
27
|
+
restoring,
|
|
28
|
+
canDeleteFile,
|
|
29
|
+
isDeleteBlocked,
|
|
30
|
+
isProtectedFile,
|
|
31
|
+
canRestoreDeletedDiff,
|
|
32
|
+
onRequestDelete,
|
|
33
|
+
onRequestRestore,
|
|
25
34
|
}) => html`
|
|
26
35
|
<div class="file-viewer-tabbar">
|
|
27
36
|
<div class="file-viewer-tab active">
|
|
@@ -58,6 +67,36 @@ export const FileViewerToolbar = ({
|
|
|
58
67
|
${!isDiffView
|
|
59
68
|
? !isImageFile && !isAudioFile && !isSqliteFile
|
|
60
69
|
? html`
|
|
70
|
+
${!isProtectedFile
|
|
71
|
+
? html`
|
|
72
|
+
<button
|
|
73
|
+
type="button"
|
|
74
|
+
onclick=${onRequestDelete}
|
|
75
|
+
disabled=${!canDeleteFile || deleting}
|
|
76
|
+
class=${`file-viewer-icon-action file-viewer-delete-action ${
|
|
77
|
+
!canDeleteFile || deleting ? "is-disabled" : ""
|
|
78
|
+
}`.trim()}
|
|
79
|
+
title=${isDeleteBlocked
|
|
80
|
+
? "Locked files cannot be deleted"
|
|
81
|
+
: "Delete file"}
|
|
82
|
+
aria-label="Delete file"
|
|
83
|
+
>
|
|
84
|
+
<${DeleteBinLineIcon} className="file-viewer-icon-action-icon" />
|
|
85
|
+
</button>
|
|
86
|
+
`
|
|
87
|
+
: null}
|
|
88
|
+
${isDirty
|
|
89
|
+
? html`
|
|
90
|
+
<${ActionButton}
|
|
91
|
+
onClick=${handleDiscard}
|
|
92
|
+
disabled=${loading || !canEditFile || isEditBlocked || deleting || saving}
|
|
93
|
+
tone="secondary"
|
|
94
|
+
size="sm"
|
|
95
|
+
idleLabel="Discard changes"
|
|
96
|
+
className="file-viewer-save-action"
|
|
97
|
+
/>
|
|
98
|
+
`
|
|
99
|
+
: null}
|
|
61
100
|
<${ActionButton}
|
|
62
101
|
onClick=${handleSave}
|
|
63
102
|
disabled=${loading || !isDirty || !canEditFile || isEditBlocked}
|
|
@@ -73,5 +112,21 @@ export const FileViewerToolbar = ({
|
|
|
73
112
|
`
|
|
74
113
|
: null
|
|
75
114
|
: null}
|
|
115
|
+
${isDiffView && canRestoreDeletedDiff
|
|
116
|
+
? html`
|
|
117
|
+
<${ActionButton}
|
|
118
|
+
onClick=${onRequestRestore}
|
|
119
|
+
disabled=${restoring}
|
|
120
|
+
loading=${restoring}
|
|
121
|
+
tone="secondary"
|
|
122
|
+
size="sm"
|
|
123
|
+
idleLabel="Restore"
|
|
124
|
+
loadingLabel="Restoring..."
|
|
125
|
+
idleIcon=${RestartLineIcon}
|
|
126
|
+
idleIconClassName="file-viewer-save-icon"
|
|
127
|
+
className="file-viewer-save-action"
|
|
128
|
+
/>
|
|
129
|
+
`
|
|
130
|
+
: null}
|
|
76
131
|
</div>
|
|
77
132
|
`;
|
|
@@ -5,6 +5,7 @@ import { getScrollRatio } from "./scroll-sync.js";
|
|
|
5
5
|
|
|
6
6
|
export const useEditorSelectionRestore = ({
|
|
7
7
|
canEditFile,
|
|
8
|
+
isEditBlocked,
|
|
8
9
|
loading,
|
|
9
10
|
hasSelectedPath,
|
|
10
11
|
normalizedPath,
|
|
@@ -18,6 +19,10 @@ export const useEditorSelectionRestore = ({
|
|
|
18
19
|
viewScrollRatioRef,
|
|
19
20
|
}) => {
|
|
20
21
|
useEffect(() => {
|
|
22
|
+
if (isEditBlocked) {
|
|
23
|
+
restoredSelectionPathRef.current = "";
|
|
24
|
+
return () => {};
|
|
25
|
+
}
|
|
21
26
|
if (!canEditFile || loading || !hasSelectedPath) return () => {};
|
|
22
27
|
if (loadedFilePathRef.current !== normalizedPath) return () => {};
|
|
23
28
|
if (restoredSelectionPathRef.current === normalizedPath) return () => {};
|
|
@@ -72,6 +77,7 @@ export const useEditorSelectionRestore = ({
|
|
|
72
77
|
};
|
|
73
78
|
}, [
|
|
74
79
|
canEditFile,
|
|
80
|
+
isEditBlocked,
|
|
75
81
|
loading,
|
|
76
82
|
hasSelectedPath,
|
|
77
83
|
normalizedPath,
|
|
@@ -10,6 +10,10 @@ export const useFileDiff = ({
|
|
|
10
10
|
const [diffLoading, setDiffLoading] = useState(false);
|
|
11
11
|
const [diffError, setDiffError] = useState("");
|
|
12
12
|
const [diffContent, setDiffContent] = useState("");
|
|
13
|
+
const [diffStatus, setDiffStatus] = useState({
|
|
14
|
+
statusKind: "",
|
|
15
|
+
isDeleted: false,
|
|
16
|
+
});
|
|
13
17
|
|
|
14
18
|
useEffect(() => {
|
|
15
19
|
let active = true;
|
|
@@ -17,6 +21,7 @@ export const useFileDiff = ({
|
|
|
17
21
|
setDiffLoading(false);
|
|
18
22
|
setDiffError("");
|
|
19
23
|
setDiffContent("");
|
|
24
|
+
setDiffStatus({ statusKind: "", isDeleted: false });
|
|
20
25
|
return () => {
|
|
21
26
|
active = false;
|
|
22
27
|
};
|
|
@@ -28,9 +33,14 @@ export const useFileDiff = ({
|
|
|
28
33
|
const data = await fetchBrowseFileDiff(normalizedPath);
|
|
29
34
|
if (!active) return;
|
|
30
35
|
setDiffContent(String(data?.content || ""));
|
|
36
|
+
setDiffStatus({
|
|
37
|
+
statusKind: String(data?.statusKind || ""),
|
|
38
|
+
isDeleted: !!data?.isDeleted,
|
|
39
|
+
});
|
|
31
40
|
} catch (nextError) {
|
|
32
41
|
if (!active) return;
|
|
33
42
|
setDiffError(nextError.message || "Could not load diff");
|
|
43
|
+
setDiffStatus({ statusKind: "", isDeleted: false });
|
|
34
44
|
} finally {
|
|
35
45
|
if (active) setDiffLoading(false);
|
|
36
46
|
}
|
|
@@ -45,5 +55,6 @@ export const useFileDiff = ({
|
|
|
45
55
|
diffLoading,
|
|
46
56
|
diffError,
|
|
47
57
|
diffContent,
|
|
58
|
+
diffStatus,
|
|
48
59
|
};
|
|
49
60
|
};
|
|
@@ -11,6 +11,7 @@ import { kFileRefreshIntervalMs, kSqlitePageSize } from "./constants.js";
|
|
|
11
11
|
export const useFileLoader = ({
|
|
12
12
|
hasSelectedPath,
|
|
13
13
|
normalizedPath,
|
|
14
|
+
isDiffView,
|
|
14
15
|
isSqliteFile,
|
|
15
16
|
sqliteSelectedTable,
|
|
16
17
|
sqliteTableOffset,
|
|
@@ -83,6 +84,14 @@ export const useFileLoader = ({
|
|
|
83
84
|
setIsFolderPath(false);
|
|
84
85
|
setExternalChangeNoticeShown(false);
|
|
85
86
|
viewScrollRatioRef.current = 0;
|
|
87
|
+
if (isDiffView) {
|
|
88
|
+
setLoading(false);
|
|
89
|
+
loadedFilePathRef.current = normalizedPath;
|
|
90
|
+
restoredSelectionPathRef.current = "";
|
|
91
|
+
return () => {
|
|
92
|
+
active = false;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
86
95
|
|
|
87
96
|
const loadFile = async () => {
|
|
88
97
|
setLoading(true);
|
|
@@ -206,7 +215,7 @@ export const useFileLoader = ({
|
|
|
206
215
|
return () => {
|
|
207
216
|
active = false;
|
|
208
217
|
};
|
|
209
|
-
}, [hasSelectedPath, normalizedPath]);
|
|
218
|
+
}, [hasSelectedPath, normalizedPath, isDiffView]);
|
|
210
219
|
|
|
211
220
|
useEffect(() => {
|
|
212
221
|
if (!isSqliteFile || !normalizedPath || !sqliteSelectedTable) {
|
|
@@ -242,7 +251,7 @@ export const useFileLoader = ({
|
|
|
242
251
|
}, [isSqliteFile, normalizedPath, sqliteSelectedTable, sqliteTableOffset]);
|
|
243
252
|
|
|
244
253
|
useEffect(() => {
|
|
245
|
-
if (!hasSelectedPath || isFolderPath || !canEditFile) return () => {};
|
|
254
|
+
if (!hasSelectedPath || isFolderPath || !canEditFile || isDiffView) return () => {};
|
|
246
255
|
const refreshFromDisk = async () => {
|
|
247
256
|
if (loading || saving) return;
|
|
248
257
|
if (fileRefreshInFlightRef.current) return;
|
|
@@ -287,6 +296,7 @@ export const useFileLoader = ({
|
|
|
287
296
|
hasSelectedPath,
|
|
288
297
|
isFolderPath,
|
|
289
298
|
canEditFile,
|
|
299
|
+
isDiffView,
|
|
290
300
|
loading,
|
|
291
301
|
saving,
|
|
292
302
|
normalizedPath,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from "https://esm.sh/preact/hooks";
|
|
2
2
|
import { marked } from "https://esm.sh/marked";
|
|
3
|
-
import { saveFileContent } from "../../lib/api.js";
|
|
3
|
+
import { deleteBrowseFile, restoreBrowseFile, saveFileContent } from "../../lib/api.js";
|
|
4
4
|
import {
|
|
5
5
|
getFileSyntaxKind,
|
|
6
6
|
highlightEditorLines,
|
|
@@ -32,6 +32,8 @@ export const useFileViewer = ({
|
|
|
32
32
|
filePath = "",
|
|
33
33
|
isPreviewOnly = false,
|
|
34
34
|
browseView = "edit",
|
|
35
|
+
onRequestClearSelection = () => {},
|
|
36
|
+
onRequestEdit = () => {},
|
|
35
37
|
}) => {
|
|
36
38
|
const normalizedPath = String(filePath || "").trim();
|
|
37
39
|
const normalizedPolicyPath = normalizeBrowsePolicyPath(normalizedPath);
|
|
@@ -50,6 +52,8 @@ export const useFileViewer = ({
|
|
|
50
52
|
const [loading, setLoading] = useState(false);
|
|
51
53
|
const [showDelayedLoadingSpinner, setShowDelayedLoadingSpinner] = useState(false);
|
|
52
54
|
const [saving, setSaving] = useState(false);
|
|
55
|
+
const [deleting, setDeleting] = useState(false);
|
|
56
|
+
const [restoring, setRestoring] = useState(false);
|
|
53
57
|
const [error, setError] = useState("");
|
|
54
58
|
const [isFolderPath, setIsFolderPath] = useState(false);
|
|
55
59
|
const [frontmatterCollapsed, setFrontmatterCollapsed] = useState(false);
|
|
@@ -83,6 +87,7 @@ export const useFileViewer = ({
|
|
|
83
87
|
const { loadedFilePathRef, restoredSelectionPathRef } = useFileLoader({
|
|
84
88
|
hasSelectedPath,
|
|
85
89
|
normalizedPath,
|
|
90
|
+
isDiffView,
|
|
86
91
|
isSqliteFile,
|
|
87
92
|
sqliteSelectedTable,
|
|
88
93
|
sqliteTableOffset,
|
|
@@ -111,7 +116,7 @@ export const useFileViewer = ({
|
|
|
111
116
|
viewScrollRatioRef,
|
|
112
117
|
});
|
|
113
118
|
|
|
114
|
-
const { diffLoading, diffError, diffContent } = useFileDiff({
|
|
119
|
+
const { diffLoading, diffError, diffContent, diffStatus } = useFileDiff({
|
|
115
120
|
hasSelectedPath,
|
|
116
121
|
isDiffView,
|
|
117
122
|
isPreviewOnly,
|
|
@@ -131,6 +136,15 @@ export const useFileViewer = ({
|
|
|
131
136
|
matchesBrowsePolicyPath(kProtectedBrowsePaths, normalizedPolicyPath);
|
|
132
137
|
const isProtectedLocked = isProtectedFile && !protectedEditBypassPaths.has(normalizedPolicyPath);
|
|
133
138
|
const isEditBlocked = isLockedFile || isProtectedLocked;
|
|
139
|
+
const isDeleteBlocked = isLockedFile || isProtectedFile;
|
|
140
|
+
const canDeleteFile =
|
|
141
|
+
hasSelectedPath &&
|
|
142
|
+
!isFolderPath &&
|
|
143
|
+
!isPreviewOnly &&
|
|
144
|
+
!isDiffView &&
|
|
145
|
+
!deleting &&
|
|
146
|
+
!saving &&
|
|
147
|
+
!isDeleteBlocked;
|
|
134
148
|
const syntaxKind = useMemo(() => getFileSyntaxKind(normalizedPath), [normalizedPath]);
|
|
135
149
|
const isMarkdownFile = syntaxKind === "markdown";
|
|
136
150
|
const shouldUseHighlightedEditor = syntaxKind !== "plain";
|
|
@@ -187,6 +201,10 @@ export const useFileViewer = ({
|
|
|
187
201
|
}
|
|
188
202
|
}, [isMarkdownFile, viewMode]);
|
|
189
203
|
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
setProtectedEditBypassPaths(new Set());
|
|
206
|
+
}, [normalizedPath]);
|
|
207
|
+
|
|
190
208
|
useEffect(() => {
|
|
191
209
|
try {
|
|
192
210
|
window.localStorage.setItem(kFileViewerModeStorageKey, viewMode);
|
|
@@ -216,6 +234,7 @@ export const useFileViewer = ({
|
|
|
216
234
|
|
|
217
235
|
useEditorSelectionRestore({
|
|
218
236
|
canEditFile,
|
|
237
|
+
isEditBlocked,
|
|
219
238
|
loading,
|
|
220
239
|
hasSelectedPath,
|
|
221
240
|
normalizedPath,
|
|
@@ -246,7 +265,6 @@ export const useFileViewer = ({
|
|
|
246
265
|
detail: { path: normalizedPath },
|
|
247
266
|
}),
|
|
248
267
|
);
|
|
249
|
-
showToast("Saved", "success");
|
|
250
268
|
} catch (saveError) {
|
|
251
269
|
const message = saveError.message || "Could not save file";
|
|
252
270
|
setError(message);
|
|
@@ -256,6 +274,72 @@ export const useFileViewer = ({
|
|
|
256
274
|
}
|
|
257
275
|
}, [canEditFile, saving, isDirty, isEditBlocked, normalizedPath, content]);
|
|
258
276
|
|
|
277
|
+
const handleDelete = useCallback(async () => {
|
|
278
|
+
if (!canDeleteFile) return;
|
|
279
|
+
setDeleting(true);
|
|
280
|
+
setError("");
|
|
281
|
+
try {
|
|
282
|
+
const data = await deleteBrowseFile(normalizedPath);
|
|
283
|
+
const deletedPath = String(data?.path || normalizedPath);
|
|
284
|
+
setExternalChangeNoticeShown(false);
|
|
285
|
+
clearStoredFileDraft(normalizedPath);
|
|
286
|
+
updateDraftIndex(normalizedPath, false, {
|
|
287
|
+
dispatchEvent: (event) => window.dispatchEvent(event),
|
|
288
|
+
});
|
|
289
|
+
window.dispatchEvent(
|
|
290
|
+
new CustomEvent("alphaclaw:browse-file-saved", {
|
|
291
|
+
detail: { path: deletedPath },
|
|
292
|
+
}),
|
|
293
|
+
);
|
|
294
|
+
window.dispatchEvent(
|
|
295
|
+
new CustomEvent("alphaclaw:browse-file-deleted", {
|
|
296
|
+
detail: { path: deletedPath },
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
window.dispatchEvent(new CustomEvent("alphaclaw:browse-tree-refresh"));
|
|
300
|
+
showToast("File deleted", "success");
|
|
301
|
+
onRequestClearSelection();
|
|
302
|
+
} catch (deleteError) {
|
|
303
|
+
const message = deleteError.message || "Could not delete file";
|
|
304
|
+
setError(message);
|
|
305
|
+
if (/path is not a file/i.test(message)) {
|
|
306
|
+
showToast("Only files can be deleted", "warning");
|
|
307
|
+
onRequestClearSelection();
|
|
308
|
+
} else {
|
|
309
|
+
showToast(message, "error");
|
|
310
|
+
}
|
|
311
|
+
} finally {
|
|
312
|
+
setDeleting(false);
|
|
313
|
+
}
|
|
314
|
+
}, [canDeleteFile, normalizedPath, onRequestClearSelection]);
|
|
315
|
+
|
|
316
|
+
const handleRestore = useCallback(async () => {
|
|
317
|
+
if (!isDiffView || !diffStatus?.isDeleted || restoring) return;
|
|
318
|
+
setRestoring(true);
|
|
319
|
+
try {
|
|
320
|
+
const data = await restoreBrowseFile(normalizedPath);
|
|
321
|
+
const restoredPath = String(data?.path || normalizedPath);
|
|
322
|
+
window.dispatchEvent(
|
|
323
|
+
new CustomEvent("alphaclaw:browse-file-saved", {
|
|
324
|
+
detail: { path: restoredPath },
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
window.dispatchEvent(new CustomEvent("alphaclaw:browse-tree-refresh"));
|
|
328
|
+
showToast("File restored", "success");
|
|
329
|
+
onRequestEdit(restoredPath);
|
|
330
|
+
} catch (restoreError) {
|
|
331
|
+
showToast(restoreError.message || "Could not restore file", "error");
|
|
332
|
+
} finally {
|
|
333
|
+
setRestoring(false);
|
|
334
|
+
}
|
|
335
|
+
}, [
|
|
336
|
+
diffStatus?.isDeleted,
|
|
337
|
+
isDiffView,
|
|
338
|
+
normalizedPath,
|
|
339
|
+
onRequestEdit,
|
|
340
|
+
restoring,
|
|
341
|
+
]);
|
|
342
|
+
|
|
259
343
|
useFileViewerHotkeys({
|
|
260
344
|
canEditFile,
|
|
261
345
|
isPreviewOnly,
|
|
@@ -281,22 +365,56 @@ export const useFileViewer = ({
|
|
|
281
365
|
});
|
|
282
366
|
};
|
|
283
367
|
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
start: event.target.selectionStart,
|
|
291
|
-
end: event.target.selectionEnd,
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
if (hasSelectedPath && canEditFile) {
|
|
368
|
+
const persistDraftForContent = useCallback(
|
|
369
|
+
(nextContent, selection = null) => {
|
|
370
|
+
if (!hasSelectedPath || !canEditFile) return;
|
|
371
|
+
if (selection) {
|
|
372
|
+
writeStoredEditorSelection(normalizedPath, selection);
|
|
373
|
+
}
|
|
295
374
|
writeStoredFileDraft(normalizedPath, nextContent);
|
|
296
375
|
updateDraftIndex(normalizedPath, nextContent !== initialContent, {
|
|
297
376
|
dispatchEvent: (event) => window.dispatchEvent(event),
|
|
298
377
|
});
|
|
299
|
-
}
|
|
378
|
+
},
|
|
379
|
+
[hasSelectedPath, canEditFile, normalizedPath, initialContent],
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const handleContentInput = (event) => {
|
|
383
|
+
if (isEditBlocked || isPreviewOnly) return;
|
|
384
|
+
const nextContent = event.target.value;
|
|
385
|
+
setContent(nextContent);
|
|
386
|
+
persistDraftForContent(nextContent, {
|
|
387
|
+
start: event.target.selectionStart,
|
|
388
|
+
end: event.target.selectionEnd,
|
|
389
|
+
});
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const handleEditorKeyDown = (event) => {
|
|
393
|
+
if (event.key !== "Tab") return;
|
|
394
|
+
if (event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
395
|
+
if (isEditBlocked || isPreviewOnly || !canEditFile) return;
|
|
396
|
+
const textareaElement = event.currentTarget;
|
|
397
|
+
if (!textareaElement) return;
|
|
398
|
+
event.preventDefault();
|
|
399
|
+
const start = Number(textareaElement.selectionStart || 0);
|
|
400
|
+
const end = Number(textareaElement.selectionEnd || 0);
|
|
401
|
+
textareaElement.setRangeText(" ", start, end, "end");
|
|
402
|
+
const nextContent = textareaElement.value;
|
|
403
|
+
setContent(nextContent);
|
|
404
|
+
persistDraftForContent(nextContent, {
|
|
405
|
+
start: textareaElement.selectionStart,
|
|
406
|
+
end: textareaElement.selectionEnd,
|
|
407
|
+
});
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const handleDiscard = () => {
|
|
411
|
+
if (!canEditFile || !isDirty || saving || deleting) return;
|
|
412
|
+
setContent(initialContent);
|
|
413
|
+
clearStoredFileDraft(normalizedPath);
|
|
414
|
+
updateDraftIndex(normalizedPath, false, {
|
|
415
|
+
dispatchEvent: (event) => window.dispatchEvent(event),
|
|
416
|
+
});
|
|
417
|
+
showToast("Changes discarded", "info");
|
|
300
418
|
};
|
|
301
419
|
|
|
302
420
|
const handleEditorSelectionChange = () => {
|
|
@@ -315,6 +433,8 @@ export const useFileViewer = ({
|
|
|
315
433
|
isPreviewOnly,
|
|
316
434
|
loading,
|
|
317
435
|
saving,
|
|
436
|
+
deleting,
|
|
437
|
+
restoring,
|
|
318
438
|
showDelayedLoadingSpinner,
|
|
319
439
|
error,
|
|
320
440
|
isFolderPath,
|
|
@@ -333,6 +453,7 @@ export const useFileViewer = ({
|
|
|
333
453
|
diffLoading,
|
|
334
454
|
diffError,
|
|
335
455
|
diffContent,
|
|
456
|
+
diffStatus,
|
|
336
457
|
isMarkdownFile,
|
|
337
458
|
frontmatterCollapsed,
|
|
338
459
|
previewHtml,
|
|
@@ -343,6 +464,8 @@ export const useFileViewer = ({
|
|
|
343
464
|
pathSegments,
|
|
344
465
|
isDirty,
|
|
345
466
|
canEditFile,
|
|
467
|
+
canDeleteFile,
|
|
468
|
+
isDeleteBlocked,
|
|
346
469
|
isEditBlocked,
|
|
347
470
|
isLockedFile,
|
|
348
471
|
isProtectedFile,
|
|
@@ -366,8 +489,12 @@ export const useFileViewer = ({
|
|
|
366
489
|
setSqliteTableOffset,
|
|
367
490
|
handleChangeViewMode,
|
|
368
491
|
handleSave,
|
|
492
|
+
handleDiscard,
|
|
493
|
+
handleDelete,
|
|
494
|
+
handleRestore,
|
|
369
495
|
handleEditProtectedFile,
|
|
370
496
|
handleContentInput,
|
|
497
|
+
handleEditorKeyDown,
|
|
371
498
|
handleEditorScroll,
|
|
372
499
|
handlePreviewScroll,
|
|
373
500
|
handleEditorSelectionChange,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import htm from "https://esm.sh/htm";
|
|
3
|
+
import { Badge } from "../badge.js";
|
|
4
|
+
import { ScopePicker } from "../scope-picker.js";
|
|
5
|
+
import { GmailWatchToggle } from "./gmail-watch-toggle.js";
|
|
6
|
+
|
|
7
|
+
const html = htm.bind(h);
|
|
8
|
+
|
|
9
|
+
const scopeListsEqual = (a = [], b = []) =>
|
|
10
|
+
a.length === b.length && a.every((scope) => b.includes(scope));
|
|
11
|
+
|
|
12
|
+
export const GoogleAccountRow = ({
|
|
13
|
+
account,
|
|
14
|
+
personal = false,
|
|
15
|
+
expanded,
|
|
16
|
+
onToggleExpanded,
|
|
17
|
+
scopes = [],
|
|
18
|
+
savedScopes = [],
|
|
19
|
+
apiStatus = {},
|
|
20
|
+
checkingApis = false,
|
|
21
|
+
onToggleScope,
|
|
22
|
+
onCheckApis,
|
|
23
|
+
onUpdatePermissions,
|
|
24
|
+
onEditCredentials,
|
|
25
|
+
onDisconnect,
|
|
26
|
+
gmailWatchStatus = null,
|
|
27
|
+
gmailWatchBusy = false,
|
|
28
|
+
onEnableGmailWatch,
|
|
29
|
+
onDisableGmailWatch,
|
|
30
|
+
onOpenGmailSetup,
|
|
31
|
+
onOpenGmailWebhook,
|
|
32
|
+
}) => {
|
|
33
|
+
const scopesChanged = !scopeListsEqual(scopes, savedScopes);
|
|
34
|
+
return html`
|
|
35
|
+
<div class="border border-border rounded-lg bg-black/20 overflow-visible">
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
onclick=${() => onToggleExpanded?.(account.id)}
|
|
39
|
+
class="w-full text-left px-3 py-2.5 flex items-center justify-between hover:bg-black/20"
|
|
40
|
+
>
|
|
41
|
+
<div class="min-w-0">
|
|
42
|
+
<div class="text-sm font-medium truncate">${account.email}</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div class="flex items-center gap-2">
|
|
45
|
+
${personal ? html`<${Badge} tone="neutral">Personal</${Badge}>` : null}
|
|
46
|
+
<${Badge} tone=${account.authenticated ? "success" : "warning"}>
|
|
47
|
+
${account.authenticated ? "Connected" : "Awaiting sign-in"}
|
|
48
|
+
</${Badge}>
|
|
49
|
+
<span class="text-xs text-gray-500">${expanded ? "▾" : "▸"}</span>
|
|
50
|
+
</div>
|
|
51
|
+
</button>
|
|
52
|
+
${expanded
|
|
53
|
+
? html`
|
|
54
|
+
<div class="px-3 pb-3 space-y-3 border-t border-border">
|
|
55
|
+
<div class="flex justify-between items-center pt-3">
|
|
56
|
+
<span class="text-sm text-gray-400">Select permissions</span>
|
|
57
|
+
${account.authenticated
|
|
58
|
+
? html`<button
|
|
59
|
+
type="button"
|
|
60
|
+
onclick=${() => onCheckApis?.(account.id)}
|
|
61
|
+
disabled=${checkingApis}
|
|
62
|
+
class="text-xs px-2 py-1 rounded-lg ac-btn-ghost disabled:opacity-50 disabled:cursor-not-allowed"
|
|
63
|
+
>
|
|
64
|
+
${checkingApis ? "Checking APIs..." : "↻ Check APIs"}
|
|
65
|
+
</button>`
|
|
66
|
+
: null}
|
|
67
|
+
</div>
|
|
68
|
+
<${ScopePicker}
|
|
69
|
+
scopes=${scopes}
|
|
70
|
+
onToggle=${(scope) => onToggleScope?.(account.id, scope)}
|
|
71
|
+
apiStatus=${account.authenticated ? apiStatus : {}}
|
|
72
|
+
loading=${account.authenticated && checkingApis}
|
|
73
|
+
/>
|
|
74
|
+
${account.authenticated
|
|
75
|
+
? html`
|
|
76
|
+
<div class="-mx-3 mt-4 mb-2 border-y border-border">
|
|
77
|
+
<div class="px-3 py-3 space-y-2">
|
|
78
|
+
<div class="flex justify-between items-center gap-2">
|
|
79
|
+
<span class="text-sm text-gray-400">Incoming events</span>
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
onclick=${() => onOpenGmailSetup?.(account.id)}
|
|
83
|
+
class="text-xs px-2 py-1 rounded-lg ac-btn-ghost"
|
|
84
|
+
>
|
|
85
|
+
Configure
|
|
86
|
+
</button>
|
|
87
|
+
</div>
|
|
88
|
+
<${GmailWatchToggle}
|
|
89
|
+
account=${account}
|
|
90
|
+
watchStatus=${gmailWatchStatus}
|
|
91
|
+
busy=${gmailWatchBusy}
|
|
92
|
+
onEnable=${() => onEnableGmailWatch?.(account.id)}
|
|
93
|
+
onDisable=${() => onDisableGmailWatch?.(account.id)}
|
|
94
|
+
onOpenWebhook=${() => onOpenGmailWebhook?.()}
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
`
|
|
99
|
+
: null}
|
|
100
|
+
<div class="pt-1 space-y-2 sm:space-y-0 sm:flex sm:justify-between sm:items-center">
|
|
101
|
+
<div class="grid grid-cols-2 gap-2 w-full sm:w-auto sm:flex sm:items-center">
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
onclick=${() => onUpdatePermissions?.(account.id)}
|
|
105
|
+
disabled=${account.authenticated && !scopesChanged}
|
|
106
|
+
class="w-full sm:w-auto text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-cyan disabled:opacity-50 disabled:cursor-not-allowed"
|
|
107
|
+
>
|
|
108
|
+
${account.authenticated ? "Update Permissions" : "Sign in with Google"}
|
|
109
|
+
</button>
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
onclick=${() => onEditCredentials?.(account.id)}
|
|
113
|
+
class="w-full sm:w-auto text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary"
|
|
114
|
+
>
|
|
115
|
+
Edit Credentials
|
|
116
|
+
</button>
|
|
117
|
+
</div>
|
|
118
|
+
<button
|
|
119
|
+
type="button"
|
|
120
|
+
onclick=${() => onDisconnect?.(account.id)}
|
|
121
|
+
class="text-xs px-2 py-1 rounded-lg ac-btn-ghost w-full sm:w-auto"
|
|
122
|
+
>
|
|
123
|
+
Disconnect
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
`
|
|
128
|
+
: null}
|
|
129
|
+
</div>
|
|
130
|
+
`;
|
|
131
|
+
};
|