@chrysb/alphaclaw 0.4.0 → 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/lib/public/css/shell.css +21 -19
- package/lib/public/css/theme.css +17 -0
- package/lib/public/js/app.js +80 -5
- package/lib/public/js/components/file-viewer/editor-surface.js +5 -0
- package/lib/public/js/components/file-viewer/index.js +3 -0
- package/lib/public/js/components/file-viewer/markdown-split-view.js +2 -0
- package/lib/public/js/components/file-viewer/toolbar.js +13 -0
- package/lib/public/js/components/file-viewer/use-file-viewer.js +48 -13
- package/lib/public/js/components/google/account-row.js +34 -1
- 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 +118 -4
- package/lib/public/js/components/google/use-gmail-watch.js +140 -0
- package/lib/public/js/components/scope-picker.js +1 -1
- package/lib/public/js/components/sidebar-git-panel.js +5 -6
- package/lib/public/js/components/sidebar.js +2 -0
- 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 +106 -1
- package/lib/public/js/lib/format.js +71 -0
- package/lib/server/constants.js +28 -0
- 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 +130 -0
- package/lib/server/helpers.js +5 -7
- package/lib/server/internal-files-migration.js +31 -3
- package/lib/server/routes/gmail.js +128 -0
- package/lib/server/routes/google.js +19 -0
- 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 +3 -0
- package/lib/setup/hourly-git-sync.sh +1 -1
- package/package.json +1 -1
- package/lib/public/js/components/usage-tab.js +0 -531
package/lib/public/css/shell.css
CHANGED
|
@@ -11,35 +11,37 @@
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
.global-restart-banner {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
position: fixed;
|
|
15
|
+
left: 50%;
|
|
16
|
+
bottom: 52px;
|
|
17
|
+
transform: translateX(-50%);
|
|
18
|
+
width: auto;
|
|
19
|
+
max-width: calc(100vw - 32px);
|
|
20
|
+
z-index: 40;
|
|
21
|
+
pointer-events: none;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
.global-restart-banner__content {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
border: 1px solid rgba(234, 179, 8, 0.35);
|
|
26
|
+
background: rgba(43, 32, 6, 0.95);
|
|
27
|
+
box-shadow: 0 18px 46px rgba(0, 0, 0, 0.42);
|
|
28
|
+
border-radius: 14px;
|
|
29
|
+
padding: 10px 14px;
|
|
25
30
|
display: flex;
|
|
26
31
|
align-items: center;
|
|
27
|
-
justify-content:
|
|
32
|
+
justify-content: space-between;
|
|
28
33
|
gap: 12px;
|
|
29
|
-
|
|
34
|
+
pointer-events: auto;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
.global-restart-banner__text {
|
|
33
38
|
font-size: 12px;
|
|
34
39
|
color: #fde68a;
|
|
35
|
-
|
|
40
|
+
line-height: 1.4;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
.global-restart-banner__button {
|
|
39
|
-
|
|
40
|
-
right: 0;
|
|
41
|
-
top: 50%;
|
|
42
|
-
transform: translateY(-50%);
|
|
44
|
+
flex-shrink: 0;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
.app-content {
|
|
@@ -292,13 +294,13 @@
|
|
|
292
294
|
grid-template-rows: auto 1fr 24px;
|
|
293
295
|
}
|
|
294
296
|
.global-restart-banner {
|
|
295
|
-
|
|
297
|
+
max-width: calc(100vw - 20px);
|
|
298
|
+
bottom: 44px;
|
|
296
299
|
}
|
|
297
300
|
.global-restart-banner__content {
|
|
298
|
-
|
|
299
|
-
align-items: flex-start;
|
|
301
|
+
align-items: stretch;
|
|
300
302
|
flex-direction: column;
|
|
301
|
-
|
|
303
|
+
gap: 8px;
|
|
302
304
|
}
|
|
303
305
|
.global-restart-banner__text {
|
|
304
306
|
text-align: left;
|
package/lib/public/css/theme.css
CHANGED
|
@@ -66,6 +66,13 @@ body::before {
|
|
|
66
66
|
background: rgba(0, 0, 0, 0.12);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/* Shared inset panel for "surface on surface" layouts. */
|
|
70
|
+
.ac-surface-inset {
|
|
71
|
+
border: 1px solid var(--panel-border-contrast);
|
|
72
|
+
border-radius: 10px;
|
|
73
|
+
background: rgba(0, 0, 0, 0.12);
|
|
74
|
+
}
|
|
75
|
+
|
|
69
76
|
.ac-history-summary {
|
|
70
77
|
cursor: pointer;
|
|
71
78
|
list-style: none;
|
|
@@ -116,6 +123,16 @@ body::before {
|
|
|
116
123
|
border-color: var(--panel-border-contrast) !important;
|
|
117
124
|
}
|
|
118
125
|
|
|
126
|
+
.ac-tip-link {
|
|
127
|
+
color: var(--accent-link);
|
|
128
|
+
text-decoration: underline;
|
|
129
|
+
text-underline-offset: 2px;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.ac-tip-link:hover {
|
|
133
|
+
color: var(--accent);
|
|
134
|
+
}
|
|
135
|
+
|
|
119
136
|
/* Universal field contrast treatment (all tabs/pages). */
|
|
120
137
|
input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
|
|
121
138
|
select,
|
package/lib/public/js/app.js
CHANGED
|
@@ -43,7 +43,7 @@ import { LoadingSpinner } from "./components/loading-spinner.js";
|
|
|
43
43
|
import { WatchdogTab } from "./components/watchdog-tab.js";
|
|
44
44
|
import { FileViewer } from "./components/file-viewer/index.js";
|
|
45
45
|
import { AppSidebar } from "./components/sidebar.js";
|
|
46
|
-
import { UsageTab } from "./components/usage-tab.js";
|
|
46
|
+
import { UsageTab } from "./components/usage-tab/index.js";
|
|
47
47
|
import { readUiSettings, writeUiSettings } from "./lib/ui-settings.js";
|
|
48
48
|
const html = htm.bind(h);
|
|
49
49
|
const kDefaultUiTab = "general";
|
|
@@ -52,7 +52,32 @@ const kSidebarMinWidthPx = 180;
|
|
|
52
52
|
const kSidebarMaxWidthPx = 460;
|
|
53
53
|
const kBrowseLastPathUiSettingKey = "browseLastPath";
|
|
54
54
|
const kLastMenuRouteUiSettingKey = "lastMenuRoute";
|
|
55
|
+
const kBrowseRestartRequiredRules = [
|
|
56
|
+
{ type: "file", path: "openclaw.json" },
|
|
57
|
+
{ type: "directory", path: "hooks/transforms" },
|
|
58
|
+
];
|
|
55
59
|
const normalizeBrowsePath = (value) => String(value || "").replace(/^\/+|\/+$/g, "");
|
|
60
|
+
const normalizeRestartRulePath = (value) =>
|
|
61
|
+
String(value || "")
|
|
62
|
+
.trim()
|
|
63
|
+
.replace(/^\/+|\/+$/g, "");
|
|
64
|
+
const matchesBrowseRestartRequiredRule = (path, rule) => {
|
|
65
|
+
const normalizedPath = normalizeRestartRulePath(path);
|
|
66
|
+
if (!normalizedPath) return false;
|
|
67
|
+
if (!rule || typeof rule !== "object") return false;
|
|
68
|
+
const type = String(rule.type || "").toLowerCase();
|
|
69
|
+
const targetPath = normalizeRestartRulePath(rule.path);
|
|
70
|
+
if (!targetPath) return false;
|
|
71
|
+
if (type === "directory") {
|
|
72
|
+
return normalizedPath === targetPath || normalizedPath.startsWith(`${targetPath}/`);
|
|
73
|
+
}
|
|
74
|
+
if (type === "file") {
|
|
75
|
+
return normalizedPath === targetPath;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
};
|
|
79
|
+
const shouldRequireRestartForBrowsePath = (path) =>
|
|
80
|
+
kBrowseRestartRequiredRules.some((rule) => matchesBrowseRestartRequiredRule(path, rule));
|
|
56
81
|
|
|
57
82
|
const clampSidebarWidth = (value) =>
|
|
58
83
|
Math.max(kSidebarMinWidthPx, Math.min(kSidebarMaxWidthPx, value));
|
|
@@ -99,6 +124,7 @@ const GeneralTab = ({
|
|
|
99
124
|
onRefreshStatuses = () => {},
|
|
100
125
|
onSwitchTab,
|
|
101
126
|
onNavigate,
|
|
127
|
+
onOpenGmailWebhook = () => {},
|
|
102
128
|
isActive,
|
|
103
129
|
restartingGateway,
|
|
104
130
|
onRestartGateway,
|
|
@@ -106,6 +132,7 @@ const GeneralTab = ({
|
|
|
106
132
|
openclawUpdateInProgress = false,
|
|
107
133
|
onOpenclawVersionActionComplete = () => {},
|
|
108
134
|
onOpenclawUpdate,
|
|
135
|
+
onRestartRequired = () => {},
|
|
109
136
|
}) => {
|
|
110
137
|
const [dashboardLoading, setDashboardLoading] = useState(false);
|
|
111
138
|
const [repairingWatchdog, setRepairingWatchdog] = useState(false);
|
|
@@ -279,7 +306,11 @@ const GeneralTab = ({
|
|
|
279
306
|
onReject=${handleReject}
|
|
280
307
|
/>
|
|
281
308
|
<${Features} onSwitchTab=${onSwitchTab} />
|
|
282
|
-
<${Google}
|
|
309
|
+
<${Google}
|
|
310
|
+
gatewayStatus=${gatewayStatus}
|
|
311
|
+
onRestartRequired=${onRestartRequired}
|
|
312
|
+
onOpenGmailWebhook=${onOpenGmailWebhook}
|
|
313
|
+
/>
|
|
283
314
|
|
|
284
315
|
${repo &&
|
|
285
316
|
html`
|
|
@@ -420,11 +451,13 @@ const App = () => {
|
|
|
420
451
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
421
452
|
const [mobileTopbarScrolled, setMobileTopbarScrolled] = useState(false);
|
|
422
453
|
const [restartRequired, setRestartRequired] = useState(false);
|
|
454
|
+
const [browseRestartRequired, setBrowseRestartRequired] = useState(false);
|
|
423
455
|
const [restartingGateway, setRestartingGateway] = useState(false);
|
|
424
456
|
const [gatewayRestartSignal, setGatewayRestartSignal] = useState(0);
|
|
425
457
|
const [statusPollCadenceMs, setStatusPollCadenceMs] = useState(15000);
|
|
426
458
|
const [openclawUpdateInProgress, setOpenclawUpdateInProgress] = useState(false);
|
|
427
459
|
const menuRef = useRef(null);
|
|
460
|
+
const routeHistoryRef = useRef([]);
|
|
428
461
|
const sharedStatusPoll = usePolling(fetchStatus, statusPollCadenceMs, {
|
|
429
462
|
enabled: onboarded === true,
|
|
430
463
|
});
|
|
@@ -433,6 +466,7 @@ const App = () => {
|
|
|
433
466
|
});
|
|
434
467
|
const sharedStatus = sharedStatusPoll.data || null;
|
|
435
468
|
const sharedWatchdogStatus = sharedWatchdogPoll.data?.status || null;
|
|
469
|
+
const isAnyRestartRequired = restartRequired || browseRestartRequired;
|
|
436
470
|
const refreshSharedStatuses = useCallback(() => {
|
|
437
471
|
sharedStatusPoll.refresh();
|
|
438
472
|
sharedWatchdogPoll.refresh();
|
|
@@ -536,6 +570,18 @@ const App = () => {
|
|
|
536
570
|
return () => clearInterval(id);
|
|
537
571
|
}, [onboarded, restartRequired, restartingGateway, refreshRestartStatus]);
|
|
538
572
|
|
|
573
|
+
useEffect(() => {
|
|
574
|
+
const handleBrowseFileSaved = (event) => {
|
|
575
|
+
const savedPath = String(event?.detail?.path || "");
|
|
576
|
+
if (!shouldRequireRestartForBrowsePath(savedPath)) return;
|
|
577
|
+
setBrowseRestartRequired(true);
|
|
578
|
+
};
|
|
579
|
+
window.addEventListener("alphaclaw:browse-file-saved", handleBrowseFileSaved);
|
|
580
|
+
return () => {
|
|
581
|
+
window.removeEventListener("alphaclaw:browse-file-saved", handleBrowseFileSaved);
|
|
582
|
+
};
|
|
583
|
+
}, []);
|
|
584
|
+
|
|
539
585
|
const handleGatewayRestart = useCallback(async () => {
|
|
540
586
|
if (restartingGateway) return;
|
|
541
587
|
setRestartingGateway(true);
|
|
@@ -543,6 +589,7 @@ const App = () => {
|
|
|
543
589
|
const data = await restartGateway();
|
|
544
590
|
if (!data?.ok) throw new Error(data?.error || "Gateway restart failed");
|
|
545
591
|
setRestartRequired(!!data.restartRequired);
|
|
592
|
+
setBrowseRestartRequired(false);
|
|
546
593
|
setGatewayRestartSignal(Date.now());
|
|
547
594
|
refreshSharedStatuses();
|
|
548
595
|
showToast("Gateway restarted", "success");
|
|
@@ -782,6 +829,16 @@ const App = () => {
|
|
|
782
829
|
setBrowsePreviewPath("");
|
|
783
830
|
}, [location]);
|
|
784
831
|
|
|
832
|
+
useEffect(() => {
|
|
833
|
+
const historyStack = routeHistoryRef.current;
|
|
834
|
+
const lastEntry = historyStack[historyStack.length - 1];
|
|
835
|
+
if (lastEntry === location) return;
|
|
836
|
+
historyStack.push(location);
|
|
837
|
+
if (historyStack.length > 100) {
|
|
838
|
+
historyStack.shift();
|
|
839
|
+
}
|
|
840
|
+
}, [location]);
|
|
841
|
+
|
|
785
842
|
useEffect(() => {
|
|
786
843
|
if (location.startsWith("/browse")) return;
|
|
787
844
|
if (location === "/telegram") return;
|
|
@@ -865,8 +922,24 @@ const App = () => {
|
|
|
865
922
|
<${Webhooks}
|
|
866
923
|
selectedHookName=${hookName}
|
|
867
924
|
onSelectHook=${(name) => setLocation(`/webhooks/${encodeURIComponent(name)}`)}
|
|
868
|
-
onBackToList=${() =>
|
|
925
|
+
onBackToList=${() => {
|
|
926
|
+
const historyStack = routeHistoryRef.current;
|
|
927
|
+
const hasPreviousRoute = historyStack.length > 1;
|
|
928
|
+
if (!hasPreviousRoute) {
|
|
929
|
+
setLocation("/webhooks");
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
const currentPath = getHashPath();
|
|
933
|
+
window.history.back();
|
|
934
|
+
window.setTimeout(() => {
|
|
935
|
+
if (getHashPath() === currentPath) {
|
|
936
|
+
setLocation("/webhooks");
|
|
937
|
+
}
|
|
938
|
+
}, 180);
|
|
939
|
+
}}
|
|
869
940
|
onRestartRequired=${setRestartRequired}
|
|
941
|
+
onOpenFile=${(relativePath) =>
|
|
942
|
+
navigateToBrowseFile(String(relativePath || "").trim(), { view: "edit" })}
|
|
870
943
|
/>
|
|
871
944
|
</div>
|
|
872
945
|
`;
|
|
@@ -878,7 +951,7 @@ const App = () => {
|
|
|
878
951
|
style=${{ "--sidebar-width": `${sidebarWidthPx}px` }}
|
|
879
952
|
>
|
|
880
953
|
<${GlobalRestartBanner}
|
|
881
|
-
visible=${
|
|
954
|
+
visible=${isAnyRestartRequired}
|
|
882
955
|
restarting=${restartingGateway}
|
|
883
956
|
onRestart=${handleGatewayRestart}
|
|
884
957
|
/>
|
|
@@ -969,6 +1042,7 @@ const App = () => {
|
|
|
969
1042
|
onRefreshStatuses=${refreshSharedStatuses}
|
|
970
1043
|
onSwitchTab=${(nextTab) => setLocation(`/${nextTab}`)}
|
|
971
1044
|
onNavigate=${navigateToSubScreen}
|
|
1045
|
+
onOpenGmailWebhook=${() => setLocation("/webhooks/gmail")}
|
|
972
1046
|
isActive=${location === "/general"}
|
|
973
1047
|
restartingGateway=${restartingGateway}
|
|
974
1048
|
onRestartGateway=${handleGatewayRestart}
|
|
@@ -976,6 +1050,7 @@ const App = () => {
|
|
|
976
1050
|
openclawUpdateInProgress=${openclawUpdateInProgress}
|
|
977
1051
|
onOpenclawVersionActionComplete=${handleOpenclawVersionActionComplete}
|
|
978
1052
|
onOpenclawUpdate=${handleOpenclawUpdate}
|
|
1053
|
+
onRestartRequired=${setRestartRequired}
|
|
979
1054
|
/>
|
|
980
1055
|
</div>
|
|
981
1056
|
</div>
|
|
@@ -1050,7 +1125,7 @@ const App = () => {
|
|
|
1050
1125
|
</div>
|
|
1051
1126
|
</div>
|
|
1052
1127
|
<${ToastContainer}
|
|
1053
|
-
className="fixed
|
|
1128
|
+
className="fixed top-4 right-4 z-50 space-y-2 pointer-events-none"
|
|
1054
1129
|
/>
|
|
1055
1130
|
</div>
|
|
1056
1131
|
|
|
@@ -8,6 +8,7 @@ const EditorTextarea = ({
|
|
|
8
8
|
editorTextareaRef,
|
|
9
9
|
renderContent,
|
|
10
10
|
handleContentInput,
|
|
11
|
+
handleEditorKeyDown,
|
|
11
12
|
handleEditorScroll,
|
|
12
13
|
handleEditorSelectionChange,
|
|
13
14
|
isEditBlocked,
|
|
@@ -18,6 +19,7 @@ const EditorTextarea = ({
|
|
|
18
19
|
ref=${editorTextareaRef}
|
|
19
20
|
value=${renderContent}
|
|
20
21
|
onInput=${handleContentInput}
|
|
22
|
+
onKeyDown=${handleEditorKeyDown}
|
|
21
23
|
onScroll=${handleEditorScroll}
|
|
22
24
|
onSelect=${handleEditorSelectionChange}
|
|
23
25
|
onKeyUp=${handleEditorSelectionChange}
|
|
@@ -48,6 +50,7 @@ export const EditorSurface = ({
|
|
|
48
50
|
editorTextareaRef,
|
|
49
51
|
renderContent,
|
|
50
52
|
handleContentInput,
|
|
53
|
+
handleEditorKeyDown,
|
|
51
54
|
handleEditorScroll,
|
|
52
55
|
handleEditorSelectionChange,
|
|
53
56
|
isEditBlocked,
|
|
@@ -97,6 +100,7 @@ export const EditorSurface = ({
|
|
|
97
100
|
editorTextareaRef=${editorTextareaRef}
|
|
98
101
|
renderContent=${renderContent}
|
|
99
102
|
handleContentInput=${handleContentInput}
|
|
103
|
+
handleEditorKeyDown=${handleEditorKeyDown}
|
|
100
104
|
handleEditorScroll=${handleEditorScroll}
|
|
101
105
|
handleEditorSelectionChange=${handleEditorSelectionChange}
|
|
102
106
|
isEditBlocked=${isEditBlocked}
|
|
@@ -110,6 +114,7 @@ export const EditorSurface = ({
|
|
|
110
114
|
editorTextareaRef=${editorTextareaRef}
|
|
111
115
|
renderContent=${renderContent}
|
|
112
116
|
handleContentInput=${handleContentInput}
|
|
117
|
+
handleEditorKeyDown=${handleEditorKeyDown}
|
|
113
118
|
handleEditorScroll=${handleEditorScroll}
|
|
114
119
|
handleEditorSelectionChange=${handleEditorSelectionChange}
|
|
115
120
|
isEditBlocked=${isEditBlocked}
|
|
@@ -52,6 +52,7 @@ export const FileViewer = ({
|
|
|
52
52
|
viewMode=${state.viewMode}
|
|
53
53
|
handleChangeViewMode=${actions.handleChangeViewMode}
|
|
54
54
|
handleSave=${actions.handleSave}
|
|
55
|
+
handleDiscard=${actions.handleDiscard}
|
|
55
56
|
loading=${state.loading}
|
|
56
57
|
canEditFile=${derived.canEditFile}
|
|
57
58
|
isEditBlocked=${derived.isEditBlocked}
|
|
@@ -153,6 +154,7 @@ export const FileViewer = ({
|
|
|
153
154
|
editorTextareaRef=${refs.editorTextareaRef}
|
|
154
155
|
renderContent=${state.renderContent}
|
|
155
156
|
handleContentInput=${actions.handleContentInput}
|
|
157
|
+
handleEditorKeyDown=${actions.handleEditorKeyDown}
|
|
156
158
|
handleEditorScroll=${actions.handleEditorScroll}
|
|
157
159
|
handleEditorSelectionChange=${actions.handleEditorSelectionChange}
|
|
158
160
|
isEditBlocked=${derived.isEditBlocked}
|
|
@@ -171,6 +173,7 @@ export const FileViewer = ({
|
|
|
171
173
|
editorTextareaRef=${refs.editorTextareaRef}
|
|
172
174
|
renderContent=${state.renderContent}
|
|
173
175
|
handleContentInput=${actions.handleContentInput}
|
|
176
|
+
handleEditorKeyDown=${actions.handleEditorKeyDown}
|
|
174
177
|
handleEditorScroll=${actions.handleEditorScroll}
|
|
175
178
|
handleEditorSelectionChange=${actions.handleEditorSelectionChange}
|
|
176
179
|
isEditBlocked=${derived.isEditBlocked}
|
|
@@ -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}
|
|
@@ -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,
|
|
@@ -84,6 +85,18 @@ export const FileViewerToolbar = ({
|
|
|
84
85
|
</button>
|
|
85
86
|
`
|
|
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}
|
|
87
100
|
<${ActionButton}
|
|
88
101
|
onClick=${handleSave}
|
|
89
102
|
disabled=${loading || !isDirty || !canEditFile || isEditBlocked}
|
|
@@ -265,7 +265,6 @@ export const useFileViewer = ({
|
|
|
265
265
|
detail: { path: normalizedPath },
|
|
266
266
|
}),
|
|
267
267
|
);
|
|
268
|
-
showToast("Saved", "success");
|
|
269
268
|
} catch (saveError) {
|
|
270
269
|
const message = saveError.message || "Could not save file";
|
|
271
270
|
setError(message);
|
|
@@ -366,22 +365,56 @@ export const useFileViewer = ({
|
|
|
366
365
|
});
|
|
367
366
|
};
|
|
368
367
|
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
start: event.target.selectionStart,
|
|
376
|
-
end: event.target.selectionEnd,
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
if (hasSelectedPath && canEditFile) {
|
|
368
|
+
const persistDraftForContent = useCallback(
|
|
369
|
+
(nextContent, selection = null) => {
|
|
370
|
+
if (!hasSelectedPath || !canEditFile) return;
|
|
371
|
+
if (selection) {
|
|
372
|
+
writeStoredEditorSelection(normalizedPath, selection);
|
|
373
|
+
}
|
|
380
374
|
writeStoredFileDraft(normalizedPath, nextContent);
|
|
381
375
|
updateDraftIndex(normalizedPath, nextContent !== initialContent, {
|
|
382
376
|
dispatchEvent: (event) => window.dispatchEvent(event),
|
|
383
377
|
});
|
|
384
|
-
}
|
|
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");
|
|
385
418
|
};
|
|
386
419
|
|
|
387
420
|
const handleEditorSelectionChange = () => {
|
|
@@ -456,10 +489,12 @@ export const useFileViewer = ({
|
|
|
456
489
|
setSqliteTableOffset,
|
|
457
490
|
handleChangeViewMode,
|
|
458
491
|
handleSave,
|
|
492
|
+
handleDiscard,
|
|
459
493
|
handleDelete,
|
|
460
494
|
handleRestore,
|
|
461
495
|
handleEditProtectedFile,
|
|
462
496
|
handleContentInput,
|
|
497
|
+
handleEditorKeyDown,
|
|
463
498
|
handleEditorScroll,
|
|
464
499
|
handlePreviewScroll,
|
|
465
500
|
handleEditorSelectionChange,
|
|
@@ -2,6 +2,7 @@ import { h } from "https://esm.sh/preact";
|
|
|
2
2
|
import htm from "https://esm.sh/htm";
|
|
3
3
|
import { Badge } from "../badge.js";
|
|
4
4
|
import { ScopePicker } from "../scope-picker.js";
|
|
5
|
+
import { GmailWatchToggle } from "./gmail-watch-toggle.js";
|
|
5
6
|
|
|
6
7
|
const html = htm.bind(h);
|
|
7
8
|
|
|
@@ -22,10 +23,16 @@ export const GoogleAccountRow = ({
|
|
|
22
23
|
onUpdatePermissions,
|
|
23
24
|
onEditCredentials,
|
|
24
25
|
onDisconnect,
|
|
26
|
+
gmailWatchStatus = null,
|
|
27
|
+
gmailWatchBusy = false,
|
|
28
|
+
onEnableGmailWatch,
|
|
29
|
+
onDisableGmailWatch,
|
|
30
|
+
onOpenGmailSetup,
|
|
31
|
+
onOpenGmailWebhook,
|
|
25
32
|
}) => {
|
|
26
33
|
const scopesChanged = !scopeListsEqual(scopes, savedScopes);
|
|
27
34
|
return html`
|
|
28
|
-
<div class="border border-border rounded-lg bg-black/20 overflow-
|
|
35
|
+
<div class="border border-border rounded-lg bg-black/20 overflow-visible">
|
|
29
36
|
<button
|
|
30
37
|
type="button"
|
|
31
38
|
onclick=${() => onToggleExpanded?.(account.id)}
|
|
@@ -64,6 +71,32 @@ export const GoogleAccountRow = ({
|
|
|
64
71
|
apiStatus=${account.authenticated ? apiStatus : {}}
|
|
65
72
|
loading=${account.authenticated && checkingApis}
|
|
66
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}
|
|
67
100
|
<div class="pt-1 space-y-2 sm:space-y-0 sm:flex sm:justify-between sm:items-center">
|
|
68
101
|
<div class="grid grid-cols-2 gap-2 w-full sm:w-auto sm:flex sm:items-center">
|
|
69
102
|
<button
|