@chrysb/alphaclaw 0.3.5-beta.1 → 0.4.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/js/app.js +126 -105
- 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/index.js +44 -6
- package/lib/public/js/components/file-viewer/status-banners.js +11 -6
- package/lib/public/js/components/file-viewer/toolbar.js +43 -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 +94 -2
- package/lib/public/js/components/google/account-row.js +98 -0
- package/lib/public/js/components/google/add-account-modal.js +93 -0
- package/lib/public/js/components/google/index.js +439 -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/sidebar-git-panel.js +43 -14
- package/lib/public/js/components/sidebar.js +91 -75
- package/lib/public/js/lib/api.js +72 -8
- package/lib/public/js/lib/browse-file-policies.js +29 -11
- 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 +19 -7
- package/lib/server/google-state.js +187 -0
- package/lib/server/helpers.js +12 -4
- 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/google.js +414 -213
- package/lib/setup/gitignore +3 -0
- package/lib/setup/hourly-git-sync.sh +28 -1
- package/package.json +1 -1
- package/lib/public/js/components/google.js +0 -228
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import { h } from "https://esm.sh/preact";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useState } from "https://esm.sh/preact/hooks";
|
|
3
|
+
import htm from "https://esm.sh/htm";
|
|
4
|
+
import {
|
|
5
|
+
checkGoogleApis,
|
|
6
|
+
disconnectGoogle,
|
|
7
|
+
saveGoogleAccount,
|
|
8
|
+
} from "../../lib/api.js";
|
|
9
|
+
import { getDefaultScopes, toggleScopeLogic } from "../scope-picker.js";
|
|
10
|
+
import { CredentialsModal } from "../credentials-modal.js";
|
|
11
|
+
import { ConfirmDialog } from "../confirm-dialog.js";
|
|
12
|
+
import { showToast } from "../toast.js";
|
|
13
|
+
import { PageHeader } from "../page-header.js";
|
|
14
|
+
import { ActionButton } from "../action-button.js";
|
|
15
|
+
import { GoogleAccountRow } from "./account-row.js";
|
|
16
|
+
import { AddGoogleAccountModal } from "./add-account-modal.js";
|
|
17
|
+
import { useGoogleAccounts } from "./use-google-accounts.js";
|
|
18
|
+
|
|
19
|
+
const html = htm.bind(h);
|
|
20
|
+
|
|
21
|
+
const hasScopesChanged = (nextScopes = [], savedScopes = []) =>
|
|
22
|
+
nextScopes.length !== savedScopes.length ||
|
|
23
|
+
nextScopes.some((scope) => !savedScopes.includes(scope));
|
|
24
|
+
|
|
25
|
+
const isPersonalAccount = (account = {}) => Boolean(account.personal);
|
|
26
|
+
|
|
27
|
+
const kGoogleIconPath = "/assets/icons/google_icon.svg";
|
|
28
|
+
|
|
29
|
+
export const Google = ({ gatewayStatus }) => {
|
|
30
|
+
const {
|
|
31
|
+
accounts,
|
|
32
|
+
loading,
|
|
33
|
+
hasCompanyCredentials,
|
|
34
|
+
refreshAccounts,
|
|
35
|
+
} = useGoogleAccounts({ gatewayStatus });
|
|
36
|
+
const [expandedAccountId, setExpandedAccountId] = useState("");
|
|
37
|
+
const [scopesByAccountId, setScopesByAccountId] = useState({});
|
|
38
|
+
const [savedScopesByAccountId, setSavedScopesByAccountId] = useState({});
|
|
39
|
+
const [apiStatusByAccountId, setApiStatusByAccountId] = useState({});
|
|
40
|
+
const [checkingByAccountId, setCheckingByAccountId] = useState({});
|
|
41
|
+
const [addMenuOpen, setAddMenuOpen] = useState(false);
|
|
42
|
+
const [credentialsModalState, setCredentialsModalState] = useState({
|
|
43
|
+
visible: false,
|
|
44
|
+
accountId: "",
|
|
45
|
+
client: "default",
|
|
46
|
+
personal: false,
|
|
47
|
+
title: "Connect Google Workspace",
|
|
48
|
+
submitLabel: "Connect Google",
|
|
49
|
+
defaultInstrType: "workspace",
|
|
50
|
+
initialValues: {},
|
|
51
|
+
});
|
|
52
|
+
const [addCompanyModalOpen, setAddCompanyModalOpen] = useState(false);
|
|
53
|
+
const [savingAddCompany, setSavingAddCompany] = useState(false);
|
|
54
|
+
const [disconnectAccountId, setDisconnectAccountId] = useState("");
|
|
55
|
+
|
|
56
|
+
const hasPersonalAccount = useMemo(
|
|
57
|
+
() => accounts.some((account) => isPersonalAccount(account)),
|
|
58
|
+
[accounts],
|
|
59
|
+
);
|
|
60
|
+
const hasCompanyAccount = useMemo(
|
|
61
|
+
() => accounts.some((account) => !isPersonalAccount(account)),
|
|
62
|
+
[accounts],
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const getAccountById = useCallback(
|
|
66
|
+
(accountId) => accounts.find((account) => account.id === accountId) || null,
|
|
67
|
+
[accounts],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const ensureScopesForAccount = useCallback((account) => {
|
|
71
|
+
const nextScopes = Array.isArray(account.activeScopes) && account.activeScopes.length
|
|
72
|
+
? account.activeScopes
|
|
73
|
+
: Array.isArray(account.services) && account.services.length
|
|
74
|
+
? account.services
|
|
75
|
+
: getDefaultScopes();
|
|
76
|
+
setSavedScopesByAccountId((prev) => ({ ...prev, [account.id]: [...nextScopes] }));
|
|
77
|
+
setScopesByAccountId((prev) => {
|
|
78
|
+
const current = prev[account.id];
|
|
79
|
+
if (!current || !hasScopesChanged(current, nextScopes)) {
|
|
80
|
+
return { ...prev, [account.id]: [...nextScopes] };
|
|
81
|
+
}
|
|
82
|
+
return prev;
|
|
83
|
+
});
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
if (!accounts.length) {
|
|
88
|
+
setExpandedAccountId("");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const firstAwaitingSignInId =
|
|
92
|
+
accounts.find((account) => !account.authenticated)?.id || "";
|
|
93
|
+
setExpandedAccountId((previousId) => {
|
|
94
|
+
if (previousId && accounts.some((account) => account.id === previousId)) {
|
|
95
|
+
return previousId;
|
|
96
|
+
}
|
|
97
|
+
return firstAwaitingSignInId;
|
|
98
|
+
});
|
|
99
|
+
accounts.forEach((account) => ensureScopesForAccount(account));
|
|
100
|
+
}, [accounts, ensureScopesForAccount]);
|
|
101
|
+
|
|
102
|
+
const startAuth = useCallback(
|
|
103
|
+
(accountId) => {
|
|
104
|
+
const account = getAccountById(accountId);
|
|
105
|
+
if (!account) return;
|
|
106
|
+
const scopes = scopesByAccountId[accountId] || account.activeScopes || getDefaultScopes();
|
|
107
|
+
if (!scopes.length) {
|
|
108
|
+
window.alert("Select at least one service");
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const authUrl =
|
|
112
|
+
`/auth/google/start?accountId=${encodeURIComponent(accountId)}` +
|
|
113
|
+
`&services=${encodeURIComponent(scopes.join(","))}&_ts=${Date.now()}`;
|
|
114
|
+
const popup = window.open(
|
|
115
|
+
authUrl,
|
|
116
|
+
`google-auth-${accountId}`,
|
|
117
|
+
"popup=yes,width=500,height=700",
|
|
118
|
+
);
|
|
119
|
+
if (!popup || popup.closed) window.location.href = authUrl;
|
|
120
|
+
},
|
|
121
|
+
[getAccountById, scopesByAccountId],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
const handleToggleScope = (accountId, scope) => {
|
|
125
|
+
setScopesByAccountId((prev) => ({
|
|
126
|
+
...prev,
|
|
127
|
+
[accountId]: toggleScopeLogic(prev[accountId] || [], scope),
|
|
128
|
+
}));
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const handleCheckApis = useCallback(async (accountId) => {
|
|
132
|
+
setApiStatusByAccountId((prev) => {
|
|
133
|
+
const next = { ...prev };
|
|
134
|
+
delete next[accountId];
|
|
135
|
+
return next;
|
|
136
|
+
});
|
|
137
|
+
setCheckingByAccountId({ [accountId]: true });
|
|
138
|
+
try {
|
|
139
|
+
const data = await checkGoogleApis(accountId);
|
|
140
|
+
if (data.results) {
|
|
141
|
+
setApiStatusByAccountId((prev) => ({ ...prev, [accountId]: data.results }));
|
|
142
|
+
}
|
|
143
|
+
} finally {
|
|
144
|
+
setCheckingByAccountId((prev) => {
|
|
145
|
+
if (!prev[accountId]) return prev;
|
|
146
|
+
const next = { ...prev };
|
|
147
|
+
delete next[accountId];
|
|
148
|
+
return next;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}, []);
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
const handler = async (event) => {
|
|
155
|
+
if (event.data?.google === "success") {
|
|
156
|
+
showToast("✓ Google account connected", "success");
|
|
157
|
+
const accountId = String(event.data?.accountId || "").trim();
|
|
158
|
+
setApiStatusByAccountId({});
|
|
159
|
+
await refreshAccounts();
|
|
160
|
+
if (accountId) {
|
|
161
|
+
await handleCheckApis(accountId);
|
|
162
|
+
}
|
|
163
|
+
} else if (event.data?.google === "error") {
|
|
164
|
+
showToast(`✗ Google auth failed: ${event.data.message || "unknown"}`, "error");
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
window.addEventListener("message", handler);
|
|
168
|
+
return () => window.removeEventListener("message", handler);
|
|
169
|
+
}, [handleCheckApis, refreshAccounts]);
|
|
170
|
+
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
if (!expandedAccountId) return;
|
|
173
|
+
const account = getAccountById(expandedAccountId);
|
|
174
|
+
if (!account?.authenticated) return;
|
|
175
|
+
if (checkingByAccountId[expandedAccountId]) return;
|
|
176
|
+
if (apiStatusByAccountId[expandedAccountId]) return;
|
|
177
|
+
handleCheckApis(expandedAccountId);
|
|
178
|
+
}, [
|
|
179
|
+
accounts,
|
|
180
|
+
apiStatusByAccountId,
|
|
181
|
+
checkingByAccountId,
|
|
182
|
+
expandedAccountId,
|
|
183
|
+
getAccountById,
|
|
184
|
+
handleCheckApis,
|
|
185
|
+
]);
|
|
186
|
+
|
|
187
|
+
const handleDisconnect = async (accountId) => {
|
|
188
|
+
const data = await disconnectGoogle(accountId);
|
|
189
|
+
if (!data.ok) {
|
|
190
|
+
showToast(`Failed to disconnect: ${data.error || "unknown"}`, "error");
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
showToast("Google account disconnected", "success");
|
|
194
|
+
setApiStatusByAccountId((prev) => {
|
|
195
|
+
const next = { ...prev };
|
|
196
|
+
delete next[accountId];
|
|
197
|
+
return next;
|
|
198
|
+
});
|
|
199
|
+
await refreshAccounts();
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const openCredentialsModal = ({
|
|
203
|
+
accountId = "",
|
|
204
|
+
client = "default",
|
|
205
|
+
personal = false,
|
|
206
|
+
title = "Connect Google Workspace",
|
|
207
|
+
submitLabel = "Connect Google",
|
|
208
|
+
defaultInstrType = personal ? "personal" : "workspace",
|
|
209
|
+
initialValues = {},
|
|
210
|
+
}) => {
|
|
211
|
+
setCredentialsModalState({
|
|
212
|
+
visible: true,
|
|
213
|
+
accountId,
|
|
214
|
+
client,
|
|
215
|
+
personal,
|
|
216
|
+
title,
|
|
217
|
+
submitLabel,
|
|
218
|
+
defaultInstrType,
|
|
219
|
+
initialValues,
|
|
220
|
+
});
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const closeCredentialsModal = () => {
|
|
224
|
+
setCredentialsModalState((prev) => ({ ...prev, visible: false }));
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const handleCredentialsSaved = async (account) => {
|
|
228
|
+
await refreshAccounts();
|
|
229
|
+
if (account?.id) startAuth(account.id);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const handleAddCompanyAccount = async ({ email, setError }) => {
|
|
233
|
+
setSavingAddCompany(true);
|
|
234
|
+
try {
|
|
235
|
+
const data = await saveGoogleAccount({
|
|
236
|
+
email,
|
|
237
|
+
client: "default",
|
|
238
|
+
personal: false,
|
|
239
|
+
services: getDefaultScopes(),
|
|
240
|
+
});
|
|
241
|
+
if (!data.ok) {
|
|
242
|
+
setError?.(data.error || "Could not add account");
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
setAddCompanyModalOpen(false);
|
|
246
|
+
await refreshAccounts();
|
|
247
|
+
if (data.accountId) startAuth(data.accountId);
|
|
248
|
+
} finally {
|
|
249
|
+
setSavingAddCompany(false);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const handleAddCompanyClick = () => {
|
|
254
|
+
setAddMenuOpen(false);
|
|
255
|
+
if (hasCompanyAccount && hasCompanyCredentials) {
|
|
256
|
+
setAddCompanyModalOpen(true);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
openCredentialsModal({
|
|
260
|
+
client: "default",
|
|
261
|
+
personal: false,
|
|
262
|
+
title: "Add Company Account",
|
|
263
|
+
submitLabel: "Save Credentials",
|
|
264
|
+
defaultInstrType: "workspace",
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const handleAddPersonalClick = () => {
|
|
269
|
+
setAddMenuOpen(false);
|
|
270
|
+
openCredentialsModal({
|
|
271
|
+
client: "personal",
|
|
272
|
+
personal: true,
|
|
273
|
+
title: "Add Personal Account",
|
|
274
|
+
submitLabel: "Save Credentials",
|
|
275
|
+
defaultInstrType: "personal",
|
|
276
|
+
});
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const handleEditCredentials = (accountId) => {
|
|
280
|
+
const account = getAccountById(accountId);
|
|
281
|
+
if (!account) return;
|
|
282
|
+
const personal = isPersonalAccount(account);
|
|
283
|
+
openCredentialsModal({
|
|
284
|
+
accountId: account.id,
|
|
285
|
+
client: personal ? "personal" : (account.client || "default"),
|
|
286
|
+
personal,
|
|
287
|
+
title: `Edit Credentials (${account.email})`,
|
|
288
|
+
submitLabel: "Save Credentials",
|
|
289
|
+
defaultInstrType: personal ? "personal" : "workspace",
|
|
290
|
+
initialValues: {
|
|
291
|
+
email: account.email,
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const renderEmptyState = () => html`
|
|
297
|
+
<div class="text-center space-y-2 py-1">
|
|
298
|
+
<div class="rounded-lg border border-border bg-black/20 px-3 py-5">
|
|
299
|
+
<div class="flex flex-col items-center justify-center gap-1.5">
|
|
300
|
+
<img
|
|
301
|
+
src=${kGoogleIconPath}
|
|
302
|
+
alt="Google logo"
|
|
303
|
+
class="h-4 w-4 shrink-0"
|
|
304
|
+
loading="lazy"
|
|
305
|
+
decoding="async"
|
|
306
|
+
/>
|
|
307
|
+
<p class="text-xs text-gray-500">
|
|
308
|
+
Connect Gmail, Calendar, Contacts, Drive, Sheets, Tasks, Docs, and Meet.
|
|
309
|
+
</p>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
|
313
|
+
<${ActionButton}
|
|
314
|
+
onClick=${handleAddCompanyClick}
|
|
315
|
+
tone="primary"
|
|
316
|
+
size="sm"
|
|
317
|
+
idleLabel="Add Company Account"
|
|
318
|
+
className="w-full font-medium"
|
|
319
|
+
/>
|
|
320
|
+
<${ActionButton}
|
|
321
|
+
onClick=${handleAddPersonalClick}
|
|
322
|
+
tone="secondary"
|
|
323
|
+
size="sm"
|
|
324
|
+
idleLabel="Add Personal Account"
|
|
325
|
+
className="w-full font-medium"
|
|
326
|
+
/>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
`;
|
|
330
|
+
|
|
331
|
+
return html`
|
|
332
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
333
|
+
<${PageHeader}
|
|
334
|
+
title="Google Accounts"
|
|
335
|
+
actions=${html`
|
|
336
|
+
${accounts.length
|
|
337
|
+
? html`
|
|
338
|
+
<div class="relative">
|
|
339
|
+
<button
|
|
340
|
+
type="button"
|
|
341
|
+
onclick=${() => setAddMenuOpen((prev) => !prev)}
|
|
342
|
+
class="text-xs font-medium px-3 py-1.5 rounded-lg ac-btn-secondary"
|
|
343
|
+
>
|
|
344
|
+
+ Add Account
|
|
345
|
+
</button>
|
|
346
|
+
${addMenuOpen
|
|
347
|
+
? html`
|
|
348
|
+
<div
|
|
349
|
+
class="absolute right-0 top-full mt-2 min-w-[210px] rounded-lg border border-border bg-modal p-1 z-20"
|
|
350
|
+
>
|
|
351
|
+
<button
|
|
352
|
+
type="button"
|
|
353
|
+
onclick=${handleAddCompanyClick}
|
|
354
|
+
class="w-full text-left px-2.5 py-1.5 text-xs rounded-md hover:bg-black/30"
|
|
355
|
+
>
|
|
356
|
+
Company account
|
|
357
|
+
</button>
|
|
358
|
+
${!hasPersonalAccount
|
|
359
|
+
? html`<button
|
|
360
|
+
type="button"
|
|
361
|
+
onclick=${handleAddPersonalClick}
|
|
362
|
+
class="w-full text-left px-2.5 py-1.5 text-xs rounded-md hover:bg-black/30"
|
|
363
|
+
>
|
|
364
|
+
Personal account
|
|
365
|
+
</button>`
|
|
366
|
+
: null}
|
|
367
|
+
</div>
|
|
368
|
+
`
|
|
369
|
+
: null}
|
|
370
|
+
</div>
|
|
371
|
+
`
|
|
372
|
+
: null}
|
|
373
|
+
`}
|
|
374
|
+
/>
|
|
375
|
+
${loading
|
|
376
|
+
? html`<div class="text-gray-500 text-sm text-center py-2">Loading...</div>`
|
|
377
|
+
: accounts.length
|
|
378
|
+
? html`
|
|
379
|
+
<div class="space-y-2 mt-3">
|
|
380
|
+
${accounts.map((account) =>
|
|
381
|
+
html`<${GoogleAccountRow}
|
|
382
|
+
key=${account.id}
|
|
383
|
+
account=${account}
|
|
384
|
+
personal=${isPersonalAccount(account)}
|
|
385
|
+
expanded=${expandedAccountId === account.id}
|
|
386
|
+
onToggleExpanded=${(accountId) =>
|
|
387
|
+
setExpandedAccountId((prev) => (prev === accountId ? "" : accountId))}
|
|
388
|
+
scopes=${scopesByAccountId[account.id] || account.activeScopes || getDefaultScopes()}
|
|
389
|
+
savedScopes=${savedScopesByAccountId[account.id] || account.activeScopes || getDefaultScopes()}
|
|
390
|
+
apiStatus=${apiStatusByAccountId[account.id] || {}}
|
|
391
|
+
checkingApis=${expandedAccountId === account.id && Boolean(checkingByAccountId[account.id])}
|
|
392
|
+
onToggleScope=${handleToggleScope}
|
|
393
|
+
onCheckApis=${handleCheckApis}
|
|
394
|
+
onUpdatePermissions=${(accountId) => startAuth(accountId)}
|
|
395
|
+
onEditCredentials=${handleEditCredentials}
|
|
396
|
+
onDisconnect=${(accountId) => setDisconnectAccountId(accountId)}
|
|
397
|
+
/>`,
|
|
398
|
+
)}
|
|
399
|
+
</div>
|
|
400
|
+
`
|
|
401
|
+
: renderEmptyState()}
|
|
402
|
+
</div>
|
|
403
|
+
|
|
404
|
+
<${CredentialsModal}
|
|
405
|
+
visible=${credentialsModalState.visible}
|
|
406
|
+
onClose=${closeCredentialsModal}
|
|
407
|
+
onSaved=${handleCredentialsSaved}
|
|
408
|
+
title=${credentialsModalState.title}
|
|
409
|
+
submitLabel=${credentialsModalState.submitLabel}
|
|
410
|
+
defaultInstrType=${credentialsModalState.defaultInstrType}
|
|
411
|
+
client=${credentialsModalState.client}
|
|
412
|
+
personal=${credentialsModalState.personal}
|
|
413
|
+
accountId=${credentialsModalState.accountId}
|
|
414
|
+
initialValues=${credentialsModalState.initialValues}
|
|
415
|
+
/>
|
|
416
|
+
|
|
417
|
+
<${AddGoogleAccountModal}
|
|
418
|
+
visible=${addCompanyModalOpen}
|
|
419
|
+
onClose=${() => setAddCompanyModalOpen(false)}
|
|
420
|
+
onSubmit=${handleAddCompanyAccount}
|
|
421
|
+
loading=${savingAddCompany}
|
|
422
|
+
title="Add Company Account"
|
|
423
|
+
/>
|
|
424
|
+
|
|
425
|
+
<${ConfirmDialog}
|
|
426
|
+
visible=${Boolean(disconnectAccountId)}
|
|
427
|
+
title="Disconnect Google account?"
|
|
428
|
+
message="Your agent will lose access to Gmail, Calendar, and other Google Workspace services until you reconnect."
|
|
429
|
+
confirmLabel="Disconnect"
|
|
430
|
+
cancelLabel="Cancel"
|
|
431
|
+
onCancel=${() => setDisconnectAccountId("")}
|
|
432
|
+
onConfirm=${async () => {
|
|
433
|
+
const accountId = disconnectAccountId;
|
|
434
|
+
setDisconnectAccountId("");
|
|
435
|
+
await handleDisconnect(accountId);
|
|
436
|
+
}}
|
|
437
|
+
/>
|
|
438
|
+
`;
|
|
439
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "https://esm.sh/preact/hooks";
|
|
2
|
+
import { fetchGoogleAccounts } from "../../lib/api.js";
|
|
3
|
+
|
|
4
|
+
export const useGoogleAccounts = ({ gatewayStatus }) => {
|
|
5
|
+
const [accounts, setAccounts] = useState([]);
|
|
6
|
+
const [loading, setLoading] = useState(true);
|
|
7
|
+
const [hasCompanyCredentials, setHasCompanyCredentials] = useState(false);
|
|
8
|
+
const [hasPersonalCredentials, setHasPersonalCredentials] = useState(false);
|
|
9
|
+
|
|
10
|
+
const refreshAccounts = useCallback(async () => {
|
|
11
|
+
setLoading(true);
|
|
12
|
+
try {
|
|
13
|
+
const data = await fetchGoogleAccounts();
|
|
14
|
+
if (data.ok) {
|
|
15
|
+
setAccounts(Array.isArray(data.accounts) ? data.accounts : []);
|
|
16
|
+
setHasCompanyCredentials(Boolean(data.hasCompanyCredentials));
|
|
17
|
+
setHasPersonalCredentials(Boolean(data.hasPersonalCredentials));
|
|
18
|
+
}
|
|
19
|
+
} finally {
|
|
20
|
+
setLoading(false);
|
|
21
|
+
}
|
|
22
|
+
}, []);
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
refreshAccounts();
|
|
26
|
+
}, [refreshAccounts]);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
if (gatewayStatus === "running") {
|
|
30
|
+
refreshAccounts();
|
|
31
|
+
}
|
|
32
|
+
}, [gatewayStatus, refreshAccounts]);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
accounts,
|
|
36
|
+
loading,
|
|
37
|
+
hasCompanyCredentials,
|
|
38
|
+
hasPersonalCredentials,
|
|
39
|
+
refreshAccounts,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
@@ -248,3 +248,29 @@ export const LockLineIcon = ({ className = "" }) => html`
|
|
|
248
248
|
/>
|
|
249
249
|
</svg>
|
|
250
250
|
`;
|
|
251
|
+
|
|
252
|
+
export const DeleteBinLineIcon = ({ className = "" }) => html`
|
|
253
|
+
<svg
|
|
254
|
+
class=${className}
|
|
255
|
+
viewBox="0 0 24 24"
|
|
256
|
+
fill="currentColor"
|
|
257
|
+
aria-hidden="true"
|
|
258
|
+
>
|
|
259
|
+
<path
|
|
260
|
+
d="M17 6H22V8H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V8H2V6H7V3C7 2.44772 7.44772 2 8 2H16C16.5523 2 17 2.44772 17 3V6ZM18 8H6V20H18V8ZM9 11H11V17H9V11ZM13 11H15V17H13V11ZM9 4V6H15V4H9Z"
|
|
261
|
+
/>
|
|
262
|
+
</svg>
|
|
263
|
+
`;
|
|
264
|
+
|
|
265
|
+
export const RestartLineIcon = ({ className = "" }) => html`
|
|
266
|
+
<svg
|
|
267
|
+
class=${className}
|
|
268
|
+
viewBox="0 0 24 24"
|
|
269
|
+
fill="currentColor"
|
|
270
|
+
aria-hidden="true"
|
|
271
|
+
>
|
|
272
|
+
<path
|
|
273
|
+
d="M18.5374 19.5674C16.7844 21.0831 14.4993 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 14.1361 21.3302 16.1158 20.1892 17.7406L17 12H20C20 7.58172 16.4183 4 12 4C7.58172 4 4 7.58172 4 12C4 16.4183 7.58172 20 12 20C14.1502 20 16.1022 19.1517 17.5398 17.7716L18.5374 19.5674Z"
|
|
274
|
+
/>
|
|
275
|
+
</svg>
|
|
276
|
+
`;
|
|
@@ -43,8 +43,8 @@ const getChangedFilePresentation = (changedFile) => {
|
|
|
43
43
|
return {
|
|
44
44
|
statusLabel: "D",
|
|
45
45
|
statusClass: "is-deleted",
|
|
46
|
-
rowClass: "",
|
|
47
|
-
canOpen:
|
|
46
|
+
rowClass: "is-clickable",
|
|
47
|
+
canOpen: true,
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
return {
|
|
@@ -62,6 +62,14 @@ const formatDelta = (value, prefix) => {
|
|
|
62
62
|
return `${prefix}${numericValue}`;
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
const isDirectoryChangePath = (changedPath, statusKind) => {
|
|
66
|
+
const safePath = String(changedPath || "").trim();
|
|
67
|
+
const safeStatusKind = String(statusKind || "").toUpperCase();
|
|
68
|
+
if (!safePath) return false;
|
|
69
|
+
if (safePath.endsWith("/")) return true;
|
|
70
|
+
return safeStatusKind === "U" && safePath.endsWith("\\");
|
|
71
|
+
};
|
|
72
|
+
|
|
65
73
|
const getRemoteSyncPresentation = (summary) => {
|
|
66
74
|
const safeState = String(summary?.syncState || "").trim();
|
|
67
75
|
const aheadCount = Number(summary?.aheadCount) || 0;
|
|
@@ -108,31 +116,41 @@ const getRemoteSyncPresentation = (summary) => {
|
|
|
108
116
|
};
|
|
109
117
|
};
|
|
110
118
|
|
|
111
|
-
const buildSyncCommitMessage = (
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const totalCount = filePaths.length;
|
|
119
|
+
const buildSyncCommitMessage = (summary) => {
|
|
120
|
+
const changedFiles = Array.isArray(summary?.changedFiles) ? summary.changedFiles : [];
|
|
121
|
+
const changedFilesCount = Number(summary?.changedFilesCount) || 0;
|
|
122
|
+
const filePaths = changedFiles
|
|
123
|
+
.map((file) => String(file?.path || "").trim())
|
|
124
|
+
.filter(Boolean);
|
|
125
|
+
const totalCount = changedFilesCount || filePaths.length;
|
|
118
126
|
if (totalCount <= 0) return "sync changes";
|
|
119
127
|
|
|
120
|
-
const fileNames = filePaths
|
|
128
|
+
const fileNames = filePaths
|
|
129
|
+
.map((filePath) => filePath.split("/").filter(Boolean).pop() || filePath);
|
|
121
130
|
const uniqueFileNames = Array.from(new Set(fileNames));
|
|
131
|
+
if (uniqueFileNames.length <= 0) {
|
|
132
|
+
const noun = totalCount === 1 ? "file" : "files";
|
|
133
|
+
return `Edited ${totalCount} ${noun}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
122
136
|
const shownFileNames = uniqueFileNames.slice(0, kSyncCommitFileNameLimit);
|
|
123
|
-
const remainingCount = Math.max(0,
|
|
137
|
+
const remainingCount = Math.max(0, totalCount - shownFileNames.length);
|
|
124
138
|
const noun = totalCount === 1 ? "file" : "files";
|
|
125
139
|
const suffix = remainingCount > 0 ? ` +${remainingCount} more` : "";
|
|
126
140
|
return `Edited ${totalCount} ${noun} - ${shownFileNames.join(", ")}${suffix}`;
|
|
127
141
|
};
|
|
128
142
|
|
|
129
|
-
export const SidebarGitPanel = ({
|
|
143
|
+
export const SidebarGitPanel = ({
|
|
144
|
+
onSelectFile = () => {},
|
|
145
|
+
isActive = true,
|
|
146
|
+
}) => {
|
|
130
147
|
const [loading, setLoading] = useState(true);
|
|
131
148
|
const [syncing, setSyncing] = useState(false);
|
|
132
149
|
const [error, setError] = useState("");
|
|
133
150
|
const [summary, setSummary] = useState(null);
|
|
134
151
|
|
|
135
152
|
useEffect(() => {
|
|
153
|
+
if (!isActive) return () => {};
|
|
136
154
|
let active = true;
|
|
137
155
|
let intervalId = null;
|
|
138
156
|
|
|
@@ -164,7 +182,7 @@ export const SidebarGitPanel = ({ onSelectFile = () => {} }) => {
|
|
|
164
182
|
if (intervalId) window.clearInterval(intervalId);
|
|
165
183
|
window.removeEventListener("alphaclaw:browse-file-saved", handleFileSaved);
|
|
166
184
|
};
|
|
167
|
-
}, []);
|
|
185
|
+
}, [isActive]);
|
|
168
186
|
|
|
169
187
|
if (loading) {
|
|
170
188
|
return html`
|
|
@@ -194,7 +212,7 @@ export const SidebarGitPanel = ({ onSelectFile = () => {} }) => {
|
|
|
194
212
|
if (!canSyncChanges || syncing) return;
|
|
195
213
|
try {
|
|
196
214
|
setSyncing(true);
|
|
197
|
-
const commitMessage = buildSyncCommitMessage(summary
|
|
215
|
+
const commitMessage = buildSyncCommitMessage(summary);
|
|
198
216
|
const syncResult = await syncBrowseChanges(commitMessage);
|
|
199
217
|
if (syncResult?.committed || syncResult?.pushed) {
|
|
200
218
|
window.dispatchEvent(new CustomEvent("alphaclaw:browse-git-synced"));
|
|
@@ -270,6 +288,17 @@ export const SidebarGitPanel = ({ onSelectFile = () => {} }) => {
|
|
|
270
288
|
title=${changedPath}
|
|
271
289
|
onclick=${() => {
|
|
272
290
|
if (!presentation.canOpen || !changedPath) return;
|
|
291
|
+
const directorySelection = isDirectoryChangePath(
|
|
292
|
+
changedPath,
|
|
293
|
+
changedFile?.statusKind,
|
|
294
|
+
);
|
|
295
|
+
if (directorySelection) {
|
|
296
|
+
onSelectFile(changedPath, {
|
|
297
|
+
directory: true,
|
|
298
|
+
preservePreview: true,
|
|
299
|
+
});
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
273
302
|
onSelectFile(changedPath, { view: "diff" });
|
|
274
303
|
}}
|
|
275
304
|
>
|