@anymux/connect 0.1.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.
Files changed (78) hide show
  1. package/dist/GitBrowser-BLgTNQyd.js +905 -0
  2. package/dist/GitBrowser-BLgTNQyd.js.map +1 -0
  3. package/dist/GitBrowser-CIyWiuX-.js +3 -0
  4. package/dist/ObjectStorageBrowser-B2YkUxMl.js +3 -0
  5. package/dist/ObjectStorageBrowser-B_25Emfu.js +267 -0
  6. package/dist/ObjectStorageBrowser-B_25Emfu.js.map +1 -0
  7. package/dist/RepoPicker-BprFGOn7.js +3 -0
  8. package/dist/RepoPicker-CoHMiJ-3.js +168 -0
  9. package/dist/RepoPicker-CoHMiJ-3.js.map +1 -0
  10. package/dist/index.d.ts +697 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +2539 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/registry.d.ts +2 -0
  15. package/dist/registry.js +3 -0
  16. package/dist/scope-labels-B4VAwoL6.js +582 -0
  17. package/dist/scope-labels-B4VAwoL6.js.map +1 -0
  18. package/dist/scope-labels-DvdJLcSL.d.ts +50 -0
  19. package/dist/scope-labels-DvdJLcSL.d.ts.map +1 -0
  20. package/package.json +87 -0
  21. package/src/adapters/adapter-registry.ts +177 -0
  22. package/src/auth/auth-client.ts +101 -0
  23. package/src/auth/token-manager.ts +27 -0
  24. package/src/components/ActionHistoryPanel.tsx +137 -0
  25. package/src/components/CapabilityCell.tsx +97 -0
  26. package/src/components/CapabilityError.tsx +50 -0
  27. package/src/components/CapabilityPanel.tsx +530 -0
  28. package/src/components/CapabilityPill.tsx +56 -0
  29. package/src/components/ConnectButton.tsx +149 -0
  30. package/src/components/ConnectedMenu.tsx +142 -0
  31. package/src/components/ConnectionStatus.tsx +28 -0
  32. package/src/components/CredentialForm.tsx +246 -0
  33. package/src/components/FullScreenBrowser.tsx +84 -0
  34. package/src/components/GitBrowser.tsx +705 -0
  35. package/src/components/GitHubRepoPicker.tsx +125 -0
  36. package/src/components/ObjectStorageBrowser.tsx +176 -0
  37. package/src/components/RepoPicker.tsx +93 -0
  38. package/src/components/ServiceCard.tsx +77 -0
  39. package/src/components/ServiceCardGrid.tsx +141 -0
  40. package/src/components/ServiceDashboard.tsx +84 -0
  41. package/src/components/ServiceIcon.tsx +37 -0
  42. package/src/components/ServiceRow.tsx +50 -0
  43. package/src/components/useAdapter.ts +33 -0
  44. package/src/demos/ServiceDashboardDemo.tsx +108 -0
  45. package/src/index.ts +68 -0
  46. package/src/models/ActionNotificationModel.ts +72 -0
  47. package/src/models/ConnectionManagerModel.ts +410 -0
  48. package/src/models/CredentialFormModel.ts +111 -0
  49. package/src/models/DashboardModel.ts +157 -0
  50. package/src/models/GitHostBrowserModel.ts +89 -0
  51. package/src/models/GitRepoBrowserModel.ts +285 -0
  52. package/src/models/ObjectStorageBrowserModel.ts +131 -0
  53. package/src/models/RepoPickerModel.ts +132 -0
  54. package/src/registry/service-registry.ts +46 -0
  55. package/src/registry/services/apple.ts +22 -0
  56. package/src/registry/services/bitbucket.ts +24 -0
  57. package/src/registry/services/box.ts +22 -0
  58. package/src/registry/services/browser-fs.ts +19 -0
  59. package/src/registry/services/dropbox.ts +22 -0
  60. package/src/registry/services/flickr.ts +22 -0
  61. package/src/registry/services/gitea.ts +24 -0
  62. package/src/registry/services/github.ts +24 -0
  63. package/src/registry/services/gitlab.ts +24 -0
  64. package/src/registry/services/google.ts +24 -0
  65. package/src/registry/services/icloud.ts +23 -0
  66. package/src/registry/services/indexeddb.ts +19 -0
  67. package/src/registry/services/instagram.ts +22 -0
  68. package/src/registry/services/microsoft.ts +24 -0
  69. package/src/registry/services/s3.ts +21 -0
  70. package/src/registry/services/webdav.ts +21 -0
  71. package/src/registry.ts +4 -0
  72. package/src/types/connection-state.ts +33 -0
  73. package/src/types/connection.ts +11 -0
  74. package/src/types/optional-deps.d.ts +149 -0
  75. package/src/types/service.ts +18 -0
  76. package/src/types/user-profile.ts +21 -0
  77. package/src/utils/action-toast.ts +53 -0
  78. package/src/utils/scope-labels.ts +91 -0
package/dist/index.js ADDED
@@ -0,0 +1,2539 @@
1
+ import { bitbucketService, browserFsService, dropboxService, getScopeLabel, getScopeLabels, giteaService, githubService, gitlabService, googleService, indexeddbService, s3Service, serviceRegistry, webdavService } from "./scope-labels-B4VAwoL6.js";
2
+ import { GitHostBrowser, GitHostBrowserModel, GitRepoBrowser, GitRepoBrowserModel, showActionToast, showErrorToast, showInfoToast } from "./GitBrowser-BLgTNQyd.js";
3
+ import { ObjectStorageBrowser, ObjectStorageBrowserModel } from "./ObjectStorageBrowser-B_25Emfu.js";
4
+ import { RepoPicker, RepoPickerModel } from "./RepoPicker-CoHMiJ-3.js";
5
+ import { createAuthClient } from "better-auth/client";
6
+ import { genericOAuthClient } from "better-auth/client/plugins";
7
+ import { flow, makeAutoObservable, runInAction } from "mobx";
8
+ import { match } from "ts-pattern";
9
+ import React, { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
10
+ import { observer } from "mobx-react-lite";
11
+ import * as icons from "lucide-react";
12
+ import { AlertTriangle, ArrowLeft, Check, Clock, Copy, ExternalLink, FileEdit, FilePlus, History, Info, Loader2, Lock, LogOut, Minus, MoreHorizontal, Move, RefreshCw, Search, Shield, Trash2, Undo2, Upload, X } from "lucide-react";
13
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
14
+ import { ConnectionBadge } from "@anymux/ui/components/connection-badge";
15
+ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@anymux/ui/components/dropdown-menu";
16
+ import { LoadingSpinner } from "@anymux/ui/components/loading-spinner";
17
+ import { PathBreadcrumb } from "@anymux/ui/components/path-breadcrumb";
18
+ import { ErrorDisplay } from "@anymux/ui/components/error-display";
19
+ import { StatusIndicator } from "@anymux/ui/components/status-indicator";
20
+
21
+ //#region src/registry/services/microsoft.ts
22
+ const microsoftService = {
23
+ id: "microsoft",
24
+ name: "Microsoft",
25
+ icon: "AppWindow",
26
+ color: "#00A4EF",
27
+ authProvider: "microsoft",
28
+ grantsUrl: "https://account.live.com/consent/Manage",
29
+ capabilities: [
30
+ {
31
+ id: "file-system",
32
+ supported: true
33
+ },
34
+ {
35
+ id: "object-storage",
36
+ supported: false
37
+ },
38
+ {
39
+ id: "git-repo",
40
+ supported: false
41
+ },
42
+ {
43
+ id: "git-host",
44
+ supported: false
45
+ },
46
+ {
47
+ id: "media",
48
+ supported: false
49
+ },
50
+ {
51
+ id: "contacts",
52
+ supported: true
53
+ },
54
+ {
55
+ id: "calendar",
56
+ supported: true
57
+ }
58
+ ],
59
+ scopes: {
60
+ "file-system": ["Files.ReadWrite.All"],
61
+ "contacts": ["Contacts.Read"],
62
+ "calendar": ["Calendars.Read"]
63
+ }
64
+ };
65
+
66
+ //#endregion
67
+ //#region src/registry/services/icloud.ts
68
+ const icloudService = {
69
+ id: "icloud",
70
+ name: "iCloud",
71
+ icon: "Cloud",
72
+ color: "#007AFF",
73
+ authProvider: "icloud",
74
+ grantsUrl: "https://appleid.apple.com/account/manage",
75
+ capabilities: [
76
+ {
77
+ id: "file-system",
78
+ supported: false
79
+ },
80
+ {
81
+ id: "object-storage",
82
+ supported: false
83
+ },
84
+ {
85
+ id: "git-repo",
86
+ supported: false
87
+ },
88
+ {
89
+ id: "git-host",
90
+ supported: false
91
+ },
92
+ {
93
+ id: "media",
94
+ supported: false
95
+ },
96
+ {
97
+ id: "contacts",
98
+ supported: true
99
+ },
100
+ {
101
+ id: "calendar",
102
+ supported: true
103
+ }
104
+ ],
105
+ scopes: {
106
+ contacts: [],
107
+ calendar: []
108
+ }
109
+ };
110
+
111
+ //#endregion
112
+ //#region src/auth/auth-client.ts
113
+ const PENDING_SERVICE_KEY = "anymux-pending-service";
114
+ function createConnectAuthClient(baseURL) {
115
+ const authClient = createAuthClient({
116
+ baseURL,
117
+ plugins: [genericOAuthClient()]
118
+ });
119
+ return {
120
+ async signIn(provider, serviceId) {
121
+ sessionStorage.setItem(PENDING_SERVICE_KEY, serviceId);
122
+ const GENERIC_OAUTH_PROVIDERS = ["box", "bitbucket"];
123
+ if (GENERIC_OAUTH_PROVIDERS.includes(provider)) await authClient.signIn.oauth2({
124
+ providerId: provider,
125
+ callbackURL: window.location.href
126
+ });
127
+ else await authClient.signIn.social({
128
+ provider,
129
+ callbackURL: window.location.href
130
+ });
131
+ },
132
+ async signOut() {
133
+ await authClient.signOut();
134
+ },
135
+ async getSession() {
136
+ const session = await authClient.getSession();
137
+ if (!session.data) return null;
138
+ return { user: {
139
+ id: session.data.user.id,
140
+ name: session.data.user.name,
141
+ ...session.data.user.image ? { image: session.data.user.image } : {}
142
+ } };
143
+ },
144
+ async getAccessToken(providerId) {
145
+ const result = await authClient.getAccessToken({ providerId });
146
+ return result.data?.accessToken ?? null;
147
+ },
148
+ async fetchConfiguredProviders() {
149
+ const res = await fetch(`${baseURL}/api/auth/configured`);
150
+ if (!res.ok) return {
151
+ database: false,
152
+ authSecret: false,
153
+ providers: {}
154
+ };
155
+ return res.json();
156
+ },
157
+ async fetchGrantedScopes() {
158
+ const res = await fetch(`${baseURL}/api/auth/scopes`, { credentials: "include" });
159
+ if (!res.ok) return {};
160
+ return res.json();
161
+ },
162
+ async fetchTestCredentials() {
163
+ const res = await fetch(`${baseURL}/api/auth/test-credentials`);
164
+ if (!res.ok) return {};
165
+ return res.json();
166
+ },
167
+ async fetchUserProfiles() {
168
+ const res = await fetch(`${baseURL}/api/auth/user-profiles`, { credentials: "include" });
169
+ if (!res.ok) return {};
170
+ return res.json();
171
+ },
172
+ async revokeProvider(providerId) {
173
+ const res = await fetch(`${baseURL}/api/auth/revoke-provider?provider=${encodeURIComponent(providerId)}`, {
174
+ method: "DELETE",
175
+ credentials: "include"
176
+ });
177
+ if (!res.ok) {
178
+ const text = await res.text().catch(() => "");
179
+ throw new Error(`revokeProvider ${providerId} failed: ${res.status} ${text}`);
180
+ }
181
+ const data = await res.json().catch(() => ({}));
182
+ console.info(`[AnyMux] revokeProvider(${providerId}): deleted=${data.deleted ?? "?"}`);
183
+ },
184
+ getPendingServiceId() {
185
+ return sessionStorage.getItem(PENDING_SERVICE_KEY);
186
+ },
187
+ clearPendingServiceId() {
188
+ sessionStorage.removeItem(PENDING_SERVICE_KEY);
189
+ }
190
+ };
191
+ }
192
+
193
+ //#endregion
194
+ //#region src/auth/token-manager.ts
195
+ const STORAGE_PREFIX = "anymux-connect-token:";
196
+ var TokenManager = class {
197
+ getToken(serviceId) {
198
+ try {
199
+ return localStorage.getItem(STORAGE_PREFIX + serviceId);
200
+ } catch {
201
+ return null;
202
+ }
203
+ }
204
+ setToken(serviceId, token) {
205
+ try {
206
+ localStorage.setItem(STORAGE_PREFIX + serviceId, token);
207
+ } catch {}
208
+ }
209
+ removeToken(serviceId) {
210
+ try {
211
+ localStorage.removeItem(STORAGE_PREFIX + serviceId);
212
+ } catch {}
213
+ }
214
+ };
215
+
216
+ //#endregion
217
+ //#region src/models/ConnectionManagerModel.ts
218
+ const STORAGE_KEY = "anymux-connect-connections";
219
+ const USER_PROFILES_STORAGE_KEY = "anymux-connect-user-profiles";
220
+ var ConnectionManagerModel = class {
221
+ constructor(options) {
222
+ this.connections = new Map();
223
+ this.grantedScopes = new Map();
224
+ this.initialized = false;
225
+ this.configuredProviders = new Set();
226
+ this.configError = null;
227
+ this.userProfiles = new Map();
228
+ this.testCredentials = {};
229
+ this.pendingReconnect = new Set();
230
+ this.tokenManager = new TokenManager();
231
+ this.disconnect = flow(function* (serviceId) {
232
+ const service = serviceRegistry.get(serviceId);
233
+ this.tokenManager.removeToken(serviceId);
234
+ this.connections.set(serviceId, {
235
+ serviceId,
236
+ status: "disconnected",
237
+ scopes: []
238
+ });
239
+ if (service) this.grantedScopes.delete(service.authProvider);
240
+ this.userProfiles.delete(serviceId);
241
+ this.persistToStorage();
242
+ if (this.authClient) {
243
+ if (service) try {
244
+ console.info(`[AnyMux] disconnect(${serviceId}): revoking provider ${service.authProvider}…`);
245
+ yield this.authClient.revokeProvider(service.authProvider);
246
+ console.info(`[AnyMux] disconnect(${serviceId}): revoke succeeded`);
247
+ } catch (err) {
248
+ console.error("[AnyMux] revokeProvider FAILED:", err instanceof Error ? err.message : err);
249
+ }
250
+ try {
251
+ yield this.authClient.signOut();
252
+ console.info(`[AnyMux] disconnect(${serviceId}): signOut succeeded`);
253
+ } catch (err) {
254
+ console.warn("[AnyMux] signOut failed:", err instanceof Error ? err.message : err);
255
+ }
256
+ }
257
+ });
258
+ this.authClient = options?.authClient ?? null;
259
+ makeAutoObservable(this);
260
+ this.loadFromStorage();
261
+ }
262
+ async initialize() {
263
+ if (this.initialized) return;
264
+ if (this.authClient) {
265
+ const pendingServiceId = this.authClient.getPendingServiceId();
266
+ if (pendingServiceId) await this.handleOAuthReturn(pendingServiceId);
267
+ }
268
+ for (const service of serviceRegistry.getAll()) if (!this.connections.has(service.id)) this.connections.set(service.id, {
269
+ serviceId: service.id,
270
+ status: "loading",
271
+ scopes: []
272
+ });
273
+ if (this.authClient) {
274
+ try {
275
+ const config = await this.authClient.fetchConfiguredProviders();
276
+ runInAction(() => {
277
+ if (!config.database) {
278
+ this.configError = "Database not configured. Set DATABASE_URL in .env.local";
279
+ return;
280
+ }
281
+ if (!config.authSecret) {
282
+ this.configError = "Auth secret not configured. Set BETTER_AUTH_SECRET in .env.local";
283
+ return;
284
+ }
285
+ for (const [provider, configured] of Object.entries(config.providers)) if (configured) this.configuredProviders.add(provider);
286
+ for (const service of serviceRegistry.getAll()) {
287
+ if (service.authProvider === "s3" || service.authProvider === "webdav" || service.authProvider === "gitea" || service.authProvider === "browser-fs" || service.authProvider === "indexeddb") continue;
288
+ if (!this.configuredProviders.has(service.authProvider) && !this.isConnected(service.id)) this.connections.set(service.id, {
289
+ serviceId: service.id,
290
+ status: "not_configured",
291
+ scopes: []
292
+ });
293
+ }
294
+ });
295
+ } catch (err) {
296
+ console.warn("[AnyMux] Failed to fetch configured providers:", err instanceof Error ? err.message : err);
297
+ runInAction(() => {
298
+ this.configError = "Failed to reach server. Check your connection.";
299
+ });
300
+ }
301
+ try {
302
+ const scopes = await this.authClient.fetchGrantedScopes();
303
+ runInAction(() => {
304
+ for (const [provider, scopeList] of Object.entries(scopes)) this.grantedScopes.set(provider, scopeList);
305
+ });
306
+ } catch (err) {
307
+ console.warn("[AnyMux] Failed to fetch granted scopes:", err instanceof Error ? err.message : err);
308
+ }
309
+ try {
310
+ const creds = await this.authClient.fetchTestCredentials();
311
+ runInAction(() => {
312
+ this.testCredentials = creds;
313
+ });
314
+ } catch (err) {
315
+ console.warn("[AnyMux] Failed to fetch test credentials:", err instanceof Error ? err.message : err);
316
+ }
317
+ }
318
+ runInAction(() => {
319
+ for (const [id, conn] of this.connections) if (conn.status === "loading") this.connections.set(id, {
320
+ ...conn,
321
+ status: "disconnected"
322
+ });
323
+ this.initialized = true;
324
+ });
325
+ if (this.authClient) this.fetchAndStoreUserProfiles();
326
+ }
327
+ /** Fetch per-provider profiles from the server and update the userProfiles map */
328
+ async fetchAndStoreUserProfiles() {
329
+ if (!this.authClient) return;
330
+ try {
331
+ const profilesByProvider = await this.authClient.fetchUserProfiles();
332
+ runInAction(() => {
333
+ for (const service of serviceRegistry.getAll()) {
334
+ const providerProfile = profilesByProvider[service.authProvider];
335
+ if (providerProfile && this.isConnected(service.id)) this.userProfiles.set(service.id, {
336
+ ...providerProfile,
337
+ provider: service.id
338
+ });
339
+ }
340
+ this.persistToStorage();
341
+ });
342
+ } catch (err) {
343
+ console.warn("[AnyMux] Failed to fetch user profiles:", err instanceof Error ? err.message : err);
344
+ }
345
+ }
346
+ async handleOAuthReturn(pendingServiceId) {
347
+ if (!this.authClient) return;
348
+ const service = serviceRegistry.get(pendingServiceId);
349
+ if (service) {
350
+ let token = null;
351
+ for (let attempt = 0; attempt < 4; attempt++) {
352
+ try {
353
+ token = await this.authClient.getAccessToken(service.authProvider);
354
+ if (token) break;
355
+ } catch {}
356
+ if (attempt < 3) await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
357
+ }
358
+ if (token) {
359
+ let fallbackProfile;
360
+ try {
361
+ const session = await this.authClient.getSession();
362
+ if (session) {
363
+ const profile = {
364
+ id: session.user.id,
365
+ name: session.user.name,
366
+ provider: pendingServiceId
367
+ };
368
+ if (session.user.image) profile.avatarUrl = session.user.image;
369
+ fallbackProfile = profile;
370
+ }
371
+ } catch (err) {
372
+ console.warn(`[AnyMux] Failed to fetch session for ${pendingServiceId}:`, err instanceof Error ? err.message : err);
373
+ }
374
+ runInAction(() => {
375
+ this.tokenManager.setToken(pendingServiceId, token);
376
+ this.connections.set(pendingServiceId, {
377
+ serviceId: pendingServiceId,
378
+ status: "connected",
379
+ accessToken: token,
380
+ scopes: Object.values(service.scopes).flat(),
381
+ connectedAt: new Date()
382
+ });
383
+ if (fallbackProfile) this.userProfiles.set(pendingServiceId, fallbackProfile);
384
+ this.persistToStorage();
385
+ });
386
+ } else try {
387
+ const session = await this.authClient.getSession();
388
+ if (session) console.warn(`[AnyMux] OAuth for ${pendingServiceId}: session exists but getAccessToken returned null after 4 attempts`);
389
+ } catch {}
390
+ }
391
+ this.authClient.clearPendingServiceId();
392
+ }
393
+ loadFromStorage() {
394
+ try {
395
+ const stored = localStorage.getItem(STORAGE_KEY);
396
+ if (stored) {
397
+ const data = JSON.parse(stored);
398
+ for (const [key, value] of data) {
399
+ const token = this.tokenManager.getToken(key);
400
+ if (token) this.connections.set(key, {
401
+ ...value,
402
+ accessToken: token
403
+ });
404
+ }
405
+ }
406
+ const storedProfiles = localStorage.getItem(USER_PROFILES_STORAGE_KEY);
407
+ if (storedProfiles) {
408
+ const entries = JSON.parse(storedProfiles);
409
+ for (const [key, value] of entries) this.userProfiles.set(key, value);
410
+ }
411
+ } catch {}
412
+ }
413
+ persistToStorage() {
414
+ try {
415
+ const data = Array.from(this.connections.entries()).map(([key, conn]) => [key, {
416
+ ...conn,
417
+ accessToken: void 0
418
+ }]);
419
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
420
+ const profileData = Array.from(this.userProfiles.entries());
421
+ localStorage.setItem(USER_PROFILES_STORAGE_KEY, JSON.stringify(profileData));
422
+ } catch {}
423
+ }
424
+ async connect(serviceId) {
425
+ const service = serviceRegistry.get(serviceId);
426
+ if (!service) return;
427
+ if (!this.authClient) {
428
+ this.connections.set(serviceId, {
429
+ serviceId,
430
+ status: "error",
431
+ scopes: []
432
+ });
433
+ return;
434
+ }
435
+ this.connections.set(serviceId, {
436
+ serviceId,
437
+ status: "connecting",
438
+ scopes: []
439
+ });
440
+ await this.authClient.signIn(service.authProvider, serviceId);
441
+ }
442
+ connectWithCredentials(serviceId, credentialToken) {
443
+ const service = serviceRegistry.get(serviceId);
444
+ if (!service) return;
445
+ this.tokenManager.setToken(serviceId, credentialToken);
446
+ this.connections.set(serviceId, {
447
+ serviceId,
448
+ status: "connected",
449
+ accessToken: credentialToken,
450
+ scopes: Object.values(service.scopes).flat(),
451
+ connectedAt: new Date()
452
+ });
453
+ this.persistToStorage();
454
+ }
455
+ requestReconnect(serviceId) {
456
+ this.pendingReconnect.add(serviceId);
457
+ }
458
+ clearReconnectRequest(serviceId) {
459
+ this.pendingReconnect.delete(serviceId);
460
+ }
461
+ isConnected(serviceId) {
462
+ return this.connections.get(serviceId)?.status === "connected";
463
+ }
464
+ getStatus(serviceId) {
465
+ return this.connections.get(serviceId)?.status ?? "disconnected";
466
+ }
467
+ getToken(serviceId) {
468
+ return this.tokenManager.getToken(serviceId);
469
+ }
470
+ /** Refresh OAuth token from better-auth. Returns fresh token or null. */
471
+ async refreshToken(serviceId) {
472
+ const service = serviceRegistry.get(serviceId);
473
+ if (!service || !this.authClient) {
474
+ console.warn(`[AnyMux] refreshToken(${serviceId}): no service or authClient`);
475
+ return null;
476
+ }
477
+ if ([
478
+ "s3",
479
+ "webdav",
480
+ "gitea",
481
+ "icloud",
482
+ "browser-fs",
483
+ "indexeddb"
484
+ ].includes(service.authProvider)) return this.tokenManager.getToken(serviceId);
485
+ const oldToken = this.tokenManager.getToken(serviceId);
486
+ try {
487
+ const token = await this.authClient.getAccessToken(service.authProvider);
488
+ if (token) {
489
+ const changed = token !== oldToken;
490
+ console.info(`[AnyMux] refreshToken(${serviceId}): got token (${token.slice(0, 8)}…${token.slice(-4)}), changed=${changed}`);
491
+ this.tokenManager.setToken(serviceId, token);
492
+ const conn = this.connections.get(serviceId);
493
+ if (conn) this.connections.set(serviceId, {
494
+ ...conn,
495
+ accessToken: token
496
+ });
497
+ return token;
498
+ }
499
+ console.warn(`[AnyMux] refreshToken(${serviceId}): getAccessToken returned null`);
500
+ } catch (err) {
501
+ console.warn(`[AnyMux] refreshToken(${serviceId}): error:`, err instanceof Error ? err.message : err);
502
+ }
503
+ console.warn(`[AnyMux] refreshToken(${serviceId}): falling back to stored token (${oldToken ? oldToken.slice(0, 8) + "…" : "null"})`);
504
+ return oldToken;
505
+ }
506
+ /** Get the full user profile for a connected service */
507
+ getUserProfile(serviceId) {
508
+ return this.userProfiles.get(serviceId);
509
+ }
510
+ /** @deprecated Use getUserProfile() instead. Kept for backward compat. */
511
+ getUserInfo(serviceId) {
512
+ const profile = this.userProfiles.get(serviceId);
513
+ if (!profile) return null;
514
+ return {
515
+ name: profile.name,
516
+ ...profile.avatarUrl ? { image: profile.avatarUrl } : {}
517
+ };
518
+ }
519
+ hasCapabilityScopes(serviceId, capabilityId) {
520
+ const service = serviceRegistry.get(serviceId);
521
+ if (!service) return false;
522
+ if (service.authProvider === "s3" || service.authProvider === "webdav" || service.authProvider === "gitea" || service.authProvider === "browser-fs" || service.authProvider === "indexeddb") return true;
523
+ const requiredScopes = service.scopes[capabilityId];
524
+ if (!requiredScopes || requiredScopes.length === 0) return false;
525
+ if (!this.grantedScopes.has(service.authProvider)) return true;
526
+ const granted = this.grantedScopes.get(service.authProvider);
527
+ return requiredScopes.every((s) => granted.includes(s));
528
+ }
529
+ };
530
+
531
+ //#endregion
532
+ //#region src/models/ActionNotificationModel.ts
533
+ let nextId = 1;
534
+ /**
535
+ * Tracks user actions for toast notifications with undo support.
536
+ * Uses sonner's toast() externally — this model just manages the action log.
537
+ */
538
+ var ActionNotificationModel = class {
539
+ constructor() {
540
+ this.actions = [];
541
+ this.maxHistory = 100;
542
+ makeAutoObservable(this, {});
543
+ }
544
+ /**
545
+ * Record an action. Returns the action ID for reference.
546
+ * The caller is responsible for showing the toast via sonner's toast().
547
+ */
548
+ record(type, description, undo) {
549
+ const id = `action-${nextId++}`;
550
+ const action = {
551
+ id,
552
+ type,
553
+ description,
554
+ timestamp: new Date(),
555
+ undo,
556
+ undone: false
557
+ };
558
+ this.actions.unshift(action);
559
+ if (this.actions.length > this.maxHistory) this.actions = this.actions.slice(0, this.maxHistory);
560
+ return id;
561
+ }
562
+ /** Mark an action as undone */
563
+ markUndone(id) {
564
+ const action = this.actions.find((a) => a.id === id);
565
+ if (action) action.undone = true;
566
+ }
567
+ get recentActions() {
568
+ return this.actions.slice(0, 20);
569
+ }
570
+ get undoableActions() {
571
+ return this.actions.filter((a) => a.undo && !a.undone);
572
+ }
573
+ clear() {
574
+ this.actions = [];
575
+ }
576
+ };
577
+
578
+ //#endregion
579
+ //#region src/models/DashboardModel.ts
580
+ const REPO_STORAGE_KEY = "anymux-selected-repos";
581
+ var DashboardModel = class {
582
+ constructor(connectionManager) {
583
+ this.selectedCell = null;
584
+ this.panelOpen = false;
585
+ this.selectedRepos = {};
586
+ this.browserPath = "/";
587
+ this.gitBrowserState = null;
588
+ this.actionNotifications = new ActionNotificationModel();
589
+ this.connectionManager = connectionManager;
590
+ makeAutoObservable(this, {
591
+ onCellChange: false,
592
+ onPathChange: false
593
+ });
594
+ try {
595
+ const stored = localStorage.getItem(REPO_STORAGE_KEY);
596
+ if (stored) this.selectedRepos = JSON.parse(stored);
597
+ const oldGithub = localStorage.getItem("anymux-github-repo");
598
+ if (oldGithub && !this.selectedRepos["github"]) {
599
+ this.selectedRepos["github"] = JSON.parse(oldGithub);
600
+ this.persistRepos();
601
+ localStorage.removeItem("anymux-github-repo");
602
+ }
603
+ } catch {}
604
+ }
605
+ /** @deprecated Use getSelectedRepo('github') instead */
606
+ get githubRepo() {
607
+ return this.selectedRepos["github"] ?? null;
608
+ }
609
+ /** @deprecated Use setSelectedRepo('github', repo) instead */
610
+ setGitHubRepo(repo) {
611
+ this.setSelectedRepo("github", repo);
612
+ }
613
+ /** @deprecated Use clearSelectedRepo('github') instead */
614
+ clearGitHubRepo() {
615
+ this.clearSelectedRepo("github");
616
+ }
617
+ setSelectedRepo(serviceId, repo) {
618
+ this.selectedRepos[serviceId] = repo;
619
+ this.persistRepos();
620
+ }
621
+ clearSelectedRepo(serviceId) {
622
+ delete this.selectedRepos[serviceId];
623
+ this.persistRepos();
624
+ }
625
+ getSelectedRepo(serviceId) {
626
+ return this.selectedRepos[serviceId] ?? null;
627
+ }
628
+ persistRepos() {
629
+ try {
630
+ localStorage.setItem(REPO_STORAGE_KEY, JSON.stringify(this.selectedRepos));
631
+ } catch {}
632
+ }
633
+ selectCell(serviceId, capabilityId) {
634
+ const service = serviceRegistry.get(serviceId);
635
+ if (!service) return;
636
+ const capability = service.capabilities.find((c) => c.id === capabilityId);
637
+ if (!capability?.supported) return;
638
+ if (capabilityId === "git-repo" || capabilityId === "git-host" || capabilityId === "file-system") this.clearSelectedRepo(serviceId);
639
+ if (!this.connectionManager.isConnected(serviceId)) return;
640
+ this.selectedCell = {
641
+ serviceId,
642
+ capabilityId
643
+ };
644
+ this.panelOpen = true;
645
+ this.onCellChange?.({
646
+ serviceId,
647
+ capabilityId
648
+ });
649
+ }
650
+ /** Open a cell without checking connection/capability (used for URL restore) */
651
+ openCell(serviceId, capabilityId) {
652
+ if (capabilityId === "git-repo" || capabilityId === "git-host" || capabilityId === "file-system") this.clearSelectedRepo(serviceId);
653
+ this.selectedCell = {
654
+ serviceId,
655
+ capabilityId
656
+ };
657
+ this.panelOpen = true;
658
+ }
659
+ setBrowserPath(path) {
660
+ this.browserPath = path;
661
+ this.onPathChange?.(path);
662
+ }
663
+ setGitBrowserState(state) {
664
+ this.gitBrowserState = state;
665
+ }
666
+ closePanel() {
667
+ this.selectedCell = null;
668
+ this.panelOpen = false;
669
+ this.browserPath = "/";
670
+ this.gitBrowserState = null;
671
+ this.onCellChange?.(null);
672
+ }
673
+ /** Close panel without triggering onCellChange (used for URL-driven state sync, e.g. browser back) */
674
+ closePanelSilent() {
675
+ this.selectedCell = null;
676
+ this.panelOpen = false;
677
+ this.browserPath = "/";
678
+ this.gitBrowserState = null;
679
+ }
680
+ /** Set browser path without triggering onPathChange (used for URL-driven state sync) */
681
+ setBrowserPathSilent(path) {
682
+ this.browserPath = path;
683
+ }
684
+ get selectedService() {
685
+ if (!this.selectedCell) return null;
686
+ return serviceRegistry.get(this.selectedCell.serviceId) ?? null;
687
+ }
688
+ get selectedCapability() {
689
+ if (!this.selectedCell || !this.selectedService) return null;
690
+ return this.selectedService.capabilities.find((c) => c.id === this.selectedCell.capabilityId) ?? null;
691
+ }
692
+ };
693
+
694
+ //#endregion
695
+ //#region src/models/CredentialFormModel.ts
696
+ var CredentialFormModel = class {
697
+ constructor() {
698
+ this.open = false;
699
+ this.serviceType = "s3";
700
+ this.accessKeyId = "";
701
+ this.secretAccessKey = "";
702
+ this.region = "us-east-1";
703
+ this.bucket = "";
704
+ this.endpoint = "";
705
+ this.url = "";
706
+ this.username = "";
707
+ this.password = "";
708
+ this.token = "";
709
+ this.owner = "";
710
+ this.repo = "";
711
+ this.email = "";
712
+ this.appPassword = "";
713
+ makeAutoObservable(this);
714
+ }
715
+ openForm(serviceType, prefill) {
716
+ this.serviceType = serviceType;
717
+ this.resetFields();
718
+ if (prefill) this.applyPrefill(prefill);
719
+ this.open = true;
720
+ }
721
+ closeForm() {
722
+ this.open = false;
723
+ }
724
+ setField(field, value) {
725
+ if (field in this) this[field] = value;
726
+ }
727
+ /** Serialize current form state to JSON credential string */
728
+ serialize() {
729
+ switch (this.serviceType) {
730
+ case "s3": {
731
+ const creds = {
732
+ accessKeyId: this.accessKeyId,
733
+ secretAccessKey: this.secretAccessKey,
734
+ region: this.region,
735
+ bucket: this.bucket
736
+ };
737
+ if (this.endpoint) creds.endpoint = this.endpoint;
738
+ return JSON.stringify(creds);
739
+ }
740
+ case "webdav": return JSON.stringify({
741
+ url: this.url,
742
+ username: this.username,
743
+ password: this.password
744
+ });
745
+ case "gitea": return JSON.stringify({
746
+ url: this.url,
747
+ username: this.username,
748
+ password: this.password,
749
+ token: this.token,
750
+ owner: this.owner,
751
+ repo: this.repo
752
+ });
753
+ case "icloud": return JSON.stringify({
754
+ email: this.email,
755
+ appPassword: this.appPassword
756
+ });
757
+ }
758
+ }
759
+ resetFields() {
760
+ this.accessKeyId = "";
761
+ this.secretAccessKey = "";
762
+ this.region = "us-east-1";
763
+ this.bucket = "";
764
+ this.endpoint = "";
765
+ this.url = "";
766
+ this.username = "";
767
+ this.password = "";
768
+ this.token = "";
769
+ this.owner = "";
770
+ this.repo = "";
771
+ this.email = "";
772
+ this.appPassword = "";
773
+ }
774
+ applyPrefill(values) {
775
+ for (const [k, v] of Object.entries(values)) if (typeof v === "string" && k in this) this[k] = v;
776
+ }
777
+ };
778
+
779
+ //#endregion
780
+ //#region src/components/ServiceIcon.tsx
781
+ /** Custom icons for services not available in lucide-react */
782
+ const AppleIcon = ({ size = 24, color = "currentColor", strokeWidth: _sw,...props }) => /* @__PURE__ */ jsx("svg", {
783
+ xmlns: "http://www.w3.org/2000/svg",
784
+ width: size,
785
+ height: size,
786
+ viewBox: "0 0 24 24",
787
+ fill: color,
788
+ ...props,
789
+ children: /* @__PURE__ */ jsx("path", { d: "M17.05 20.28c-.98.95-2.05.88-3.08.4-1.09-.5-2.08-.48-3.24 0-1.44.62-2.2.44-3.06-.4C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" })
790
+ });
791
+ const customIcons = { Apple: AppleIcon };
792
+ const ServiceIcon = ({ name,...props }) => {
793
+ const CustomIcon = customIcons[name];
794
+ if (CustomIcon) return /* @__PURE__ */ jsx(CustomIcon, { ...props });
795
+ const Icon = icons[name];
796
+ if (!Icon) return /* @__PURE__ */ jsx(icons.HelpCircle, { ...props });
797
+ return /* @__PURE__ */ jsx(Icon, { ...props });
798
+ };
799
+
800
+ //#endregion
801
+ //#region src/types/connection-state.ts
802
+ function getServiceConnectionState(connectionManager, serviceId) {
803
+ const status = connectionManager.getStatus(serviceId);
804
+ return match(status).with("loading", () => ({ status: "loading" })).with("not_configured", () => ({ status: "not_configured" })).with("disconnected", () => ({ status: "disconnected" })).with("connecting", () => ({ status: "connecting" })).with("connected", () => ({
805
+ status: "connected",
806
+ token: connectionManager.getToken(serviceId) ?? "",
807
+ user: connectionManager.getUserInfo(serviceId),
808
+ profile: connectionManager.getUserProfile(serviceId)
809
+ })).with("error", () => ({ status: "error" })).with("expired", () => ({ status: "expired" })).exhaustive();
810
+ }
811
+
812
+ //#endregion
813
+ //#region src/components/CredentialForm.tsx
814
+ const CredentialForm = observer(({ model, onSubmit }) => {
815
+ if (!model.open) return null;
816
+ const handleSubmit = (e) => {
817
+ e.preventDefault();
818
+ onSubmit(model.serialize());
819
+ };
820
+ const inputClass = "w-full rounded-md border border-border px-3 py-2 text-base sm:text-sm bg-card text-foreground focus:border-ring focus:outline-none focus:ring-1 focus:ring-ring";
821
+ const labelClass = "block text-sm font-medium text-foreground mb-1";
822
+ const title = model.serviceType === "s3" ? "S3 Credentials" : model.serviceType === "webdav" ? "WebDAV Credentials" : model.serviceType === "icloud" ? "iCloud Credentials" : "Gitea Credentials";
823
+ return /* @__PURE__ */ jsx("div", {
824
+ className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50",
825
+ onClick: () => model.closeForm(),
826
+ children: /* @__PURE__ */ jsxs("div", {
827
+ className: "w-[calc(100%-2rem)] max-w-md max-h-[80vh] flex flex-col rounded-lg bg-card shadow-xl",
828
+ onClick: (e) => e.stopPropagation(),
829
+ children: [/* @__PURE__ */ jsx("h2", {
830
+ className: "px-4 sm:px-6 pt-4 sm:pt-6 pb-3 text-lg font-semibold text-foreground flex-shrink-0",
831
+ children: title
832
+ }), /* @__PURE__ */ jsxs("form", {
833
+ onSubmit: handleSubmit,
834
+ className: "flex flex-col flex-1 min-h-0",
835
+ children: [/* @__PURE__ */ jsx("div", {
836
+ className: "flex-1 overflow-y-auto px-4 sm:px-6 space-y-3",
837
+ children: model.serviceType === "s3" ? /* @__PURE__ */ jsxs(Fragment, { children: [
838
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
839
+ className: labelClass,
840
+ children: "Access Key ID"
841
+ }), /* @__PURE__ */ jsx("input", {
842
+ type: "text",
843
+ required: true,
844
+ className: inputClass,
845
+ value: model.accessKeyId,
846
+ onChange: (e) => model.setField("accessKeyId", e.target.value)
847
+ })] }),
848
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
849
+ className: labelClass,
850
+ children: "Secret Access Key"
851
+ }), /* @__PURE__ */ jsx("input", {
852
+ type: "password",
853
+ required: true,
854
+ className: inputClass,
855
+ value: model.secretAccessKey,
856
+ onChange: (e) => model.setField("secretAccessKey", e.target.value)
857
+ })] }),
858
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
859
+ className: labelClass,
860
+ children: "Region"
861
+ }), /* @__PURE__ */ jsx("input", {
862
+ type: "text",
863
+ required: true,
864
+ className: inputClass,
865
+ value: model.region,
866
+ onChange: (e) => model.setField("region", e.target.value)
867
+ })] }),
868
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
869
+ className: labelClass,
870
+ children: "Bucket"
871
+ }), /* @__PURE__ */ jsx("input", {
872
+ type: "text",
873
+ required: true,
874
+ className: inputClass,
875
+ value: model.bucket,
876
+ onChange: (e) => model.setField("bucket", e.target.value)
877
+ })] }),
878
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
879
+ className: labelClass,
880
+ children: "Endpoint (optional)"
881
+ }), /* @__PURE__ */ jsx("input", {
882
+ type: "text",
883
+ className: inputClass,
884
+ value: model.endpoint,
885
+ onChange: (e) => model.setField("endpoint", e.target.value),
886
+ placeholder: "https://s3.amazonaws.com"
887
+ })] })
888
+ ] }) : model.serviceType === "webdav" ? /* @__PURE__ */ jsxs(Fragment, { children: [
889
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
890
+ className: labelClass,
891
+ children: "URL"
892
+ }), /* @__PURE__ */ jsx("input", {
893
+ type: "url",
894
+ required: true,
895
+ className: inputClass,
896
+ value: model.url,
897
+ onChange: (e) => model.setField("url", e.target.value),
898
+ placeholder: "https://example.com/webdav"
899
+ })] }),
900
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
901
+ className: labelClass,
902
+ children: "Username"
903
+ }), /* @__PURE__ */ jsx("input", {
904
+ type: "text",
905
+ required: true,
906
+ className: inputClass,
907
+ value: model.username,
908
+ onChange: (e) => model.setField("username", e.target.value)
909
+ })] }),
910
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
911
+ className: labelClass,
912
+ children: "Password"
913
+ }), /* @__PURE__ */ jsx("input", {
914
+ type: "password",
915
+ required: true,
916
+ className: inputClass,
917
+ value: model.password,
918
+ onChange: (e) => model.setField("password", e.target.value)
919
+ })] })
920
+ ] }) : model.serviceType === "icloud" ? /* @__PURE__ */ jsxs(Fragment, { children: [
921
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
922
+ className: labelClass,
923
+ children: "Apple ID Email"
924
+ }), /* @__PURE__ */ jsx("input", {
925
+ type: "email",
926
+ required: true,
927
+ className: inputClass,
928
+ value: model.email,
929
+ onChange: (e) => model.setField("email", e.target.value),
930
+ placeholder: "you@icloud.com"
931
+ })] }),
932
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
933
+ className: labelClass,
934
+ children: "App-Specific Password"
935
+ }), /* @__PURE__ */ jsx("input", {
936
+ type: "password",
937
+ required: true,
938
+ className: inputClass,
939
+ value: model.appPassword,
940
+ onChange: (e) => model.setField("appPassword", e.target.value),
941
+ placeholder: "xxxx-xxxx-xxxx-xxxx"
942
+ })] }),
943
+ /* @__PURE__ */ jsxs("p", {
944
+ className: "text-xs text-muted-foreground",
945
+ children: [
946
+ "Generate an app-specific password at",
947
+ " ",
948
+ /* @__PURE__ */ jsx("a", {
949
+ href: "https://appleid.apple.com/account/manage",
950
+ target: "_blank",
951
+ rel: "noopener noreferrer",
952
+ className: "underline text-primary hover:text-primary/80",
953
+ children: "appleid.apple.com"
954
+ }),
955
+ " ",
956
+ "under Sign-In and Security → App-Specific Passwords."
957
+ ]
958
+ })
959
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
960
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
961
+ className: labelClass,
962
+ children: "URL"
963
+ }), /* @__PURE__ */ jsx("input", {
964
+ type: "url",
965
+ required: true,
966
+ className: inputClass,
967
+ value: model.url,
968
+ onChange: (e) => model.setField("url", e.target.value),
969
+ placeholder: "https://gitea.example.com"
970
+ })] }),
971
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
972
+ className: labelClass,
973
+ children: "Token (or use username/password below)"
974
+ }), /* @__PURE__ */ jsx("input", {
975
+ type: "password",
976
+ className: inputClass,
977
+ value: model.token,
978
+ onChange: (e) => model.setField("token", e.target.value),
979
+ placeholder: "API token"
980
+ })] }),
981
+ !model.token && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
982
+ className: labelClass,
983
+ children: "Username"
984
+ }), /* @__PURE__ */ jsx("input", {
985
+ type: "text",
986
+ className: inputClass,
987
+ value: model.username,
988
+ onChange: (e) => model.setField("username", e.target.value)
989
+ })] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
990
+ className: labelClass,
991
+ children: "Password"
992
+ }), /* @__PURE__ */ jsx("input", {
993
+ type: "password",
994
+ className: inputClass,
995
+ value: model.password,
996
+ onChange: (e) => model.setField("password", e.target.value)
997
+ })] })] }),
998
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
999
+ className: labelClass,
1000
+ children: "Owner"
1001
+ }), /* @__PURE__ */ jsx("input", {
1002
+ type: "text",
1003
+ required: true,
1004
+ className: inputClass,
1005
+ value: model.owner,
1006
+ onChange: (e) => model.setField("owner", e.target.value)
1007
+ })] }),
1008
+ /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("label", {
1009
+ className: labelClass,
1010
+ children: "Repo"
1011
+ }), /* @__PURE__ */ jsx("input", {
1012
+ type: "text",
1013
+ required: true,
1014
+ className: inputClass,
1015
+ value: model.repo,
1016
+ onChange: (e) => model.setField("repo", e.target.value)
1017
+ })] })
1018
+ ] })
1019
+ }), /* @__PURE__ */ jsxs("div", {
1020
+ className: "flex justify-end gap-2 px-4 sm:px-6 py-3 border-t border-border flex-shrink-0",
1021
+ children: [/* @__PURE__ */ jsx("button", {
1022
+ type: "button",
1023
+ onClick: () => model.closeForm(),
1024
+ className: "rounded-md px-4 py-2 text-sm font-medium text-muted-foreground hover:bg-muted",
1025
+ children: "Cancel"
1026
+ }), /* @__PURE__ */ jsx("button", {
1027
+ type: "submit",
1028
+ className: "rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90",
1029
+ children: "Connect"
1030
+ })]
1031
+ })]
1032
+ })]
1033
+ })
1034
+ });
1035
+ });
1036
+
1037
+ //#endregion
1038
+ //#region src/components/ConnectedMenu.tsx
1039
+ function formatConnectedDate(date) {
1040
+ if (!date) return null;
1041
+ const d = date instanceof Date ? date : new Date(date);
1042
+ if (isNaN(d.getTime())) return null;
1043
+ return d.toLocaleDateString(void 0, {
1044
+ month: "short",
1045
+ day: "numeric",
1046
+ year: "numeric"
1047
+ });
1048
+ }
1049
+ function ConnectedMenu({ service, connectionManager, user, profile, isOAuth }) {
1050
+ const displayName = profile?.name ?? user?.name;
1051
+ const avatarUrl = profile?.avatarUrl ?? user?.image;
1052
+ const profileUrl = profile?.profileUrl;
1053
+ const email = profile?.email;
1054
+ const handleReconnect = async () => {
1055
+ await connectionManager.disconnect(service.id);
1056
+ await connectionManager.connect(service.id);
1057
+ };
1058
+ const handleDisconnect = () => {
1059
+ connectionManager.disconnect(service.id);
1060
+ };
1061
+ const connection = connectionManager.connections.get(service.id);
1062
+ const connectedDate = formatConnectedDate(connection?.connectedAt);
1063
+ return /* @__PURE__ */ jsxs("div", {
1064
+ className: "inline-flex items-center gap-1",
1065
+ children: [/* @__PURE__ */ jsx(ConnectionBadge, {
1066
+ status: "connected",
1067
+ ...displayName ? { name: displayName } : {},
1068
+ ...avatarUrl ? { avatarUrl } : {}
1069
+ }), /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
1070
+ asChild: true,
1071
+ children: /* @__PURE__ */ jsx("button", {
1072
+ className: "inline-flex items-center justify-center rounded-md p-1 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors",
1073
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "h-4 w-4" })
1074
+ })
1075
+ }), /* @__PURE__ */ jsxs(DropdownMenuContent, {
1076
+ align: "end",
1077
+ className: "min-w-[200px]",
1078
+ children: [
1079
+ /* @__PURE__ */ jsxs(DropdownMenuLabel, {
1080
+ className: "flex items-center gap-2",
1081
+ children: [avatarUrl ? /* @__PURE__ */ jsx("img", {
1082
+ src: avatarUrl,
1083
+ alt: "",
1084
+ className: "h-5 w-5 rounded-full shrink-0"
1085
+ }) : /* @__PURE__ */ jsx(ServiceIcon, {
1086
+ name: service.icon,
1087
+ className: "h-4 w-4 shrink-0",
1088
+ style: { color: service.color }
1089
+ }), /* @__PURE__ */ jsxs("div", {
1090
+ className: "flex flex-col min-w-0",
1091
+ children: [/* @__PURE__ */ jsx("span", {
1092
+ className: "truncate",
1093
+ children: displayName ?? service.name
1094
+ }), email && /* @__PURE__ */ jsx("span", {
1095
+ className: "text-xs font-normal text-muted-foreground truncate",
1096
+ children: email
1097
+ })]
1098
+ })]
1099
+ }),
1100
+ connectedDate && /* @__PURE__ */ jsxs("div", {
1101
+ className: "flex items-center gap-2 px-2 pb-1.5 text-xs text-muted-foreground",
1102
+ children: [/* @__PURE__ */ jsx(Clock, { className: "h-3 w-3 shrink-0" }), /* @__PURE__ */ jsxs("span", { children: ["Connected ", connectedDate] })]
1103
+ }),
1104
+ /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
1105
+ profileUrl && /* @__PURE__ */ jsx(DropdownMenuItem, {
1106
+ asChild: true,
1107
+ children: /* @__PURE__ */ jsxs("a", {
1108
+ href: profileUrl,
1109
+ target: "_blank",
1110
+ rel: "noopener noreferrer",
1111
+ children: [/* @__PURE__ */ jsx(ExternalLink, {}), "Open Profile"]
1112
+ })
1113
+ }),
1114
+ service.grantsUrl && /* @__PURE__ */ jsx(DropdownMenuItem, {
1115
+ asChild: true,
1116
+ children: /* @__PURE__ */ jsxs("a", {
1117
+ href: service.grantsUrl,
1118
+ target: "_blank",
1119
+ rel: "noopener noreferrer",
1120
+ children: [/* @__PURE__ */ jsx(Shield, {}), "Manage Permissions"]
1121
+ })
1122
+ }),
1123
+ /* @__PURE__ */ jsx(DropdownMenuItem, {
1124
+ asChild: true,
1125
+ children: /* @__PURE__ */ jsxs("a", {
1126
+ href: `/providers/${service.id}`,
1127
+ children: [
1128
+ /* @__PURE__ */ jsx(Info, {}),
1129
+ "About ",
1130
+ service.name
1131
+ ]
1132
+ })
1133
+ }),
1134
+ isOAuth && /* @__PURE__ */ jsxs(DropdownMenuItem, {
1135
+ onClick: handleReconnect,
1136
+ children: [/* @__PURE__ */ jsx(RefreshCw, {}), "Reconnect"]
1137
+ }),
1138
+ /* @__PURE__ */ jsxs(DropdownMenuItem, {
1139
+ variant: "destructive",
1140
+ onClick: handleDisconnect,
1141
+ children: [/* @__PURE__ */ jsx(LogOut, {}), "Disconnect"]
1142
+ })
1143
+ ]
1144
+ })] })]
1145
+ });
1146
+ }
1147
+
1148
+ //#endregion
1149
+ //#region src/components/ConnectButton.tsx
1150
+ const ConnectButton = observer(({ service, connectionManager }) => {
1151
+ const [formModel] = useState(() => new CredentialFormModel());
1152
+ const state = getServiceConnectionState(connectionManager, service.id);
1153
+ const needsCredentialForm = service.authProvider === "s3" || service.authProvider === "webdav" || service.authProvider === "gitea" || service.authProvider === "icloud";
1154
+ const isBrowserFs = service.authProvider === "browser-fs";
1155
+ const isIndexedDb = service.authProvider === "indexeddb";
1156
+ const pendingReconnect = connectionManager.pendingReconnect.has(service.id);
1157
+ useEffect(() => {
1158
+ if (pendingReconnect) {
1159
+ connectionManager.clearReconnectRequest(service.id);
1160
+ handleConnect();
1161
+ }
1162
+ }, [pendingReconnect]);
1163
+ const handleConnect = async () => {
1164
+ if (isBrowserFs) {
1165
+ try {
1166
+ const { BrowserFileSystemFactory } = await import("@anymux/browser-fs");
1167
+ const factory = new BrowserFileSystemFactory();
1168
+ const [_fs, handleInfo] = await factory.createFromPicker();
1169
+ connectionManager.connectWithCredentials(service.id, JSON.stringify({
1170
+ id: handleInfo.id,
1171
+ name: handleInfo.name
1172
+ }));
1173
+ } catch (err) {
1174
+ if (err.name !== "AbortError") console.error("Failed to pick directory:", err);
1175
+ }
1176
+ return;
1177
+ }
1178
+ if (isIndexedDb) {
1179
+ connectionManager.connectWithCredentials(service.id, JSON.stringify({ type: "indexeddb" }));
1180
+ return;
1181
+ }
1182
+ if (needsCredentialForm) formModel.openForm(service.authProvider);
1183
+ else connectionManager.connect(service.id);
1184
+ };
1185
+ const handleCredentialSubmit = (credentialToken) => {
1186
+ connectionManager.connectWithCredentials(service.id, credentialToken);
1187
+ formModel.closeForm();
1188
+ };
1189
+ const credentialFormElement = needsCredentialForm ? /* @__PURE__ */ jsx(CredentialForm, {
1190
+ model: formModel,
1191
+ onSubmit: handleCredentialSubmit
1192
+ }) : null;
1193
+ return match(state).with({ status: "loading" }, () => /* @__PURE__ */ jsx(ConnectionBadge, { status: "loading" })).with({ status: "not_configured" }, () => /* @__PURE__ */ jsx(ConnectionBadge, { status: "not-configured" })).with({ status: "connecting" }, () => /* @__PURE__ */ jsx(ConnectionBadge, { status: "connecting" })).with({ status: "connected" }, (s) => {
1194
+ const isOAuth = !needsCredentialForm && !isBrowserFs && !isIndexedDb;
1195
+ const user = isOAuth ? s.user : null;
1196
+ return /* @__PURE__ */ jsx(ConnectedMenu, {
1197
+ service,
1198
+ connectionManager,
1199
+ user,
1200
+ ...s.profile ? { profile: s.profile } : {},
1201
+ isOAuth
1202
+ });
1203
+ }).with({ status: "error" }, () => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
1204
+ onClick: handleConnect,
1205
+ className: "rounded-md px-3 py-1.5 text-xs font-medium text-white bg-destructive hover:bg-destructive/90 transition-colors",
1206
+ children: "Retry"
1207
+ }), credentialFormElement] })).with({ status: "expired" }, () => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
1208
+ onClick: handleConnect,
1209
+ className: "rounded-md px-3 py-1.5 text-xs font-medium text-white bg-destructive hover:bg-destructive/90 transition-colors",
1210
+ children: "Reconnect"
1211
+ }), credentialFormElement] })).with({ status: "disconnected" }, () => {
1212
+ const testCreds = connectionManager.testCredentials[service.authProvider];
1213
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
1214
+ className: "inline-flex items-center gap-1.5",
1215
+ children: [/* @__PURE__ */ jsx("button", {
1216
+ onClick: handleConnect,
1217
+ className: "rounded-md px-3 py-1.5 text-xs font-medium text-white transition-colors",
1218
+ style: { backgroundColor: service.color },
1219
+ children: "Connect"
1220
+ }), testCreds && /* @__PURE__ */ jsx("button", {
1221
+ onClick: () => {
1222
+ if (needsCredentialForm) {
1223
+ const values = {};
1224
+ for (const [k, v] of Object.entries(testCreds)) if (typeof v === "string") values[k] = v;
1225
+ formModel.openForm(service.authProvider, values);
1226
+ } else connectionManager.connectWithCredentials(service.id, JSON.stringify(testCreds));
1227
+ },
1228
+ className: "rounded-md px-2 py-1.5 text-xs font-medium text-muted-foreground bg-muted hover:bg-muted/80 transition-colors",
1229
+ children: "Quick Test"
1230
+ })]
1231
+ }), credentialFormElement] });
1232
+ }).exhaustive();
1233
+ });
1234
+
1235
+ //#endregion
1236
+ //#region src/components/CapabilityCell.tsx
1237
+ const CapabilityCell = observer(({ service, capabilityId, connectionManager, dashboardModel }) => {
1238
+ const capability = service.capabilities.find((c) => c.id === capabilityId);
1239
+ const gitHostCap = capabilityId === "git-repo" ? service.capabilities.find((c) => c.id === "git-host") : void 0;
1240
+ const supported = (capability?.supported ?? false) || (gitHostCap?.supported ?? false);
1241
+ const effectiveCapabilityId = capabilityId === "git-repo" && !capability?.supported && gitHostCap?.supported ? "git-host" : capabilityId;
1242
+ const status = connectionManager.getStatus(service.id);
1243
+ const isSelected = dashboardModel.selectedCell?.serviceId === service.id && (dashboardModel.selectedCell?.capabilityId === effectiveCapabilityId || capabilityId === "git-repo" && dashboardModel.selectedCell?.capabilityId === "git-host");
1244
+ if (!supported) return /* @__PURE__ */ jsx("td", {
1245
+ className: "px-3 py-2 text-center align-middle",
1246
+ children: /* @__PURE__ */ jsx(Minus, { className: "h-4 w-4 text-muted-foreground/30 mx-auto" })
1247
+ });
1248
+ const handleKeyDown = (e) => {
1249
+ if (e.key === "Enter" || e.key === " ") {
1250
+ e.preventDefault();
1251
+ dashboardModel.selectCell(service.id, effectiveCapabilityId);
1252
+ }
1253
+ };
1254
+ return match(status).with("loading", () => /* @__PURE__ */ jsx("td", {
1255
+ className: "px-3 py-2 text-center align-middle",
1256
+ children: /* @__PURE__ */ jsx("span", { className: "inline-block h-4 w-4 rounded bg-muted animate-pulse mx-auto" })
1257
+ })).with("connected", () => {
1258
+ if (!connectionManager.initialized) return /* @__PURE__ */ jsx("td", {
1259
+ className: "px-3 py-2 text-center align-middle",
1260
+ children: /* @__PURE__ */ jsx("span", { className: "inline-block h-4 w-4 rounded bg-muted animate-pulse mx-auto" })
1261
+ });
1262
+ const hasScopes = connectionManager.hasCapabilityScopes(service.id, effectiveCapabilityId);
1263
+ const hoverBg = hasScopes ? "hover:bg-green-50 dark:hover:bg-green-900/20" : "hover:bg-orange-50 dark:hover:bg-orange-900/20";
1264
+ return /* @__PURE__ */ jsx("td", {
1265
+ className: `px-3 py-2 text-center align-middle cursor-pointer transition-colors ${isSelected ? "bg-blue-100 dark:bg-blue-900/40" : hoverBg}`,
1266
+ role: "button",
1267
+ tabIndex: 0,
1268
+ onClick: () => dashboardModel.selectCell(service.id, effectiveCapabilityId),
1269
+ onKeyDown: handleKeyDown,
1270
+ title: hasScopes ? void 0 : "Scope not granted",
1271
+ children: /* @__PURE__ */ jsx("span", {
1272
+ className: `inline-flex items-center justify-center w-5 h-5 rounded-full mx-auto ${hasScopes ? "bg-green-500" : "bg-orange-400"} text-white`,
1273
+ children: /* @__PURE__ */ jsx(Check, { className: "h-3 w-3" })
1274
+ })
1275
+ });
1276
+ }).otherwise(() => /* @__PURE__ */ jsx("td", {
1277
+ className: "px-3 py-2 text-center align-middle",
1278
+ children: /* @__PURE__ */ jsx("span", {
1279
+ className: "inline-flex items-center justify-center w-5 h-5 rounded border border-muted-foreground/30 mx-auto",
1280
+ children: /* @__PURE__ */ jsx(Check, { className: "h-3 w-3 text-muted-foreground/50" })
1281
+ })
1282
+ }));
1283
+ });
1284
+
1285
+ //#endregion
1286
+ //#region src/components/ServiceRow.tsx
1287
+ const CAPABILITY_COLUMNS$1 = [
1288
+ "file-system",
1289
+ "object-storage",
1290
+ "git-repo",
1291
+ "media",
1292
+ "contacts",
1293
+ "calendar"
1294
+ ];
1295
+ const ServiceRow = observer(({ service, connectionManager, dashboardModel }) => {
1296
+ return /* @__PURE__ */ jsxs("tr", {
1297
+ className: "border-b border-border h-11",
1298
+ children: [
1299
+ /* @__PURE__ */ jsx("td", {
1300
+ className: "px-3 py-2 align-middle",
1301
+ children: /* @__PURE__ */ jsxs("div", {
1302
+ className: "flex items-center gap-2",
1303
+ children: [/* @__PURE__ */ jsx(ServiceIcon, {
1304
+ name: service.icon,
1305
+ className: "h-4 w-4",
1306
+ style: { color: service.color }
1307
+ }), /* @__PURE__ */ jsx("a", {
1308
+ href: `/providers/${service.id}`,
1309
+ className: "text-sm font-medium truncate hover:underline",
1310
+ title: service.name,
1311
+ children: service.name
1312
+ })]
1313
+ })
1314
+ }),
1315
+ /* @__PURE__ */ jsx("td", {
1316
+ className: "px-3 py-2 align-middle",
1317
+ children: /* @__PURE__ */ jsx(ConnectButton, {
1318
+ service,
1319
+ connectionManager
1320
+ })
1321
+ }),
1322
+ CAPABILITY_COLUMNS$1.map((cap) => /* @__PURE__ */ jsx(CapabilityCell, {
1323
+ service,
1324
+ capabilityId: cap,
1325
+ connectionManager,
1326
+ dashboardModel
1327
+ }, cap))
1328
+ ]
1329
+ });
1330
+ });
1331
+
1332
+ //#endregion
1333
+ //#region src/components/CapabilityPill.tsx
1334
+ const CapabilityPill = ({ label, status, isSelected, hasScopes, onSelect, disabled = false }) => {
1335
+ const connected = status === "connected";
1336
+ const loading = status === "loading";
1337
+ const className = `inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium transition-colors ${loading ? "bg-muted text-muted-foreground" : isSelected ? "bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300" : connected ? "bg-muted text-foreground hover:bg-muted/80 cursor-pointer" : "bg-muted/50 text-muted-foreground"}`;
1338
+ const icon = match(status).with("loading", () => /* @__PURE__ */ jsx(LoadingSpinner, {
1339
+ size: "sm",
1340
+ label: "Loading",
1341
+ className: "h-3 w-3"
1342
+ })).with("connected", () => /* @__PURE__ */ jsx(Check, { className: `h-3 w-3 ${hasScopes ? "text-green-500" : "text-orange-400"}` })).otherwise(() => /* @__PURE__ */ jsx(Minus, { className: "h-3 w-3" }));
1343
+ return /* @__PURE__ */ jsxs("button", {
1344
+ onClick: onSelect,
1345
+ disabled: disabled || !connected,
1346
+ className,
1347
+ children: [icon, label]
1348
+ });
1349
+ };
1350
+
1351
+ //#endregion
1352
+ //#region src/components/ServiceCard.tsx
1353
+ const CAPABILITY_COLUMNS = [
1354
+ {
1355
+ id: "file-system",
1356
+ label: "FS"
1357
+ },
1358
+ {
1359
+ id: "object-storage",
1360
+ label: "Obj"
1361
+ },
1362
+ {
1363
+ id: "git-repo",
1364
+ label: "Git"
1365
+ },
1366
+ {
1367
+ id: "media",
1368
+ label: "Media"
1369
+ },
1370
+ {
1371
+ id: "contacts",
1372
+ label: "Contacts"
1373
+ },
1374
+ {
1375
+ id: "calendar",
1376
+ label: "Cal"
1377
+ }
1378
+ ];
1379
+ const ServiceCard = observer(({ service, connectionManager, dashboardModel }) => {
1380
+ const status = connectionManager.getStatus(service.id);
1381
+ const connected = status === "connected";
1382
+ const supportedCaps = CAPABILITY_COLUMNS.filter((cap) => {
1383
+ const supported = service.capabilities.find((c) => c.id === cap.id)?.supported;
1384
+ if (cap.id === "git-repo" && !supported) return service.capabilities.find((c) => c.id === "git-host")?.supported ?? false;
1385
+ return supported;
1386
+ });
1387
+ return /* @__PURE__ */ jsxs("div", {
1388
+ className: "rounded-lg border border-border bg-card p-3",
1389
+ children: [/* @__PURE__ */ jsxs("div", {
1390
+ className: "flex items-center justify-between gap-2 mb-2",
1391
+ children: [/* @__PURE__ */ jsxs("div", {
1392
+ className: "flex items-center gap-2 min-w-0",
1393
+ children: [/* @__PURE__ */ jsx(ServiceIcon, {
1394
+ name: service.icon,
1395
+ className: "h-5 w-5 flex-shrink-0",
1396
+ style: { color: service.color }
1397
+ }), /* @__PURE__ */ jsx("span", {
1398
+ className: "text-sm font-medium truncate",
1399
+ title: service.name,
1400
+ children: service.name
1401
+ })]
1402
+ }), /* @__PURE__ */ jsx(ConnectButton, {
1403
+ service,
1404
+ connectionManager
1405
+ })]
1406
+ }), supportedCaps.length > 0 && /* @__PURE__ */ jsx("div", {
1407
+ className: "flex flex-wrap gap-1.5",
1408
+ children: supportedCaps.map((cap) => {
1409
+ const isSelected = dashboardModel.selectedCell?.serviceId === service.id && dashboardModel.selectedCell?.capabilityId === cap.id;
1410
+ const hasScopes = connected && connectionManager.hasCapabilityScopes(service.id, cap.id);
1411
+ return /* @__PURE__ */ jsx(CapabilityPill, {
1412
+ label: cap.label,
1413
+ capabilityId: cap.id,
1414
+ status,
1415
+ isSelected,
1416
+ hasScopes,
1417
+ onSelect: () => {
1418
+ if (connected) dashboardModel.selectCell(service.id, cap.id);
1419
+ }
1420
+ }, cap.id);
1421
+ })
1422
+ })]
1423
+ });
1424
+ });
1425
+
1426
+ //#endregion
1427
+ //#region src/adapters/adapter-registry.ts
1428
+ const factories = new Map();
1429
+ const adapterRegistry = {
1430
+ register(serviceId, factory) {
1431
+ factories.set(serviceId, factory);
1432
+ },
1433
+ async create(serviceId, token, capabilityId, context = {}) {
1434
+ const factory = factories.get(serviceId);
1435
+ if (!factory) throw new Error(`No adapter factory registered for service: ${serviceId}`);
1436
+ return factory(token, capabilityId, context);
1437
+ },
1438
+ has(serviceId) {
1439
+ return factories.has(serviceId);
1440
+ }
1441
+ };
1442
+ adapterRegistry.register("google", async (token, capabilityId) => {
1443
+ const m = await import("@anymux/google-drive");
1444
+ const adapters = {
1445
+ "file-system": () => new m.GoogleDriveFileSystem({ accessToken: token }),
1446
+ "contacts": () => new m.GoogleContactsProvider(token),
1447
+ "calendar": () => new m.GoogleCalendarProvider(token)
1448
+ };
1449
+ return adapters[capabilityId]();
1450
+ });
1451
+ adapterRegistry.register("dropbox", async (token) => {
1452
+ const { DropboxFileSystem } = await import("@anymux/dropbox");
1453
+ return new DropboxFileSystem({ accessToken: token });
1454
+ });
1455
+ adapterRegistry.register("box", async (token) => {
1456
+ const { BoxFileSystem } = await import("@anymux/box");
1457
+ return new BoxFileSystem({ accessToken: token });
1458
+ });
1459
+ adapterRegistry.register("github", async (token, capabilityId, ctx) => {
1460
+ const repo = ctx.selectedRepo;
1461
+ if (!repo) return null;
1462
+ const config = {
1463
+ token,
1464
+ owner: repo.owner,
1465
+ repo: repo.repo,
1466
+ branch: ctx.branch ?? "main"
1467
+ };
1468
+ const m = await import("@anymux/github");
1469
+ const adapters = {
1470
+ "file-system": () => new m.GitHubFileSystem(config),
1471
+ "git-repo": () => new m.GitHubGitRepo(config),
1472
+ "git-host": () => new m.GitHubGitHost(config)
1473
+ };
1474
+ return adapters[capabilityId]();
1475
+ });
1476
+ adapterRegistry.register("gitlab", async (token, capabilityId, ctx) => {
1477
+ const repo = ctx.selectedRepo;
1478
+ if (!repo) return null;
1479
+ const config = {
1480
+ token,
1481
+ projectId: `${repo.owner}/${repo.repo}`,
1482
+ branch: ctx.branch ?? "main"
1483
+ };
1484
+ const m = await import("@anymux/gitlab");
1485
+ const adapters = {
1486
+ "file-system": () => new m.GitLabFileSystem(config),
1487
+ "git-repo": () => new m.GitLabGitRepo(config),
1488
+ "git-host": () => new m.GitLabGitHost(config)
1489
+ };
1490
+ return adapters[capabilityId]();
1491
+ });
1492
+ adapterRegistry.register("bitbucket", async (token, capabilityId, ctx) => {
1493
+ const repo = ctx.selectedRepo;
1494
+ if (!repo) return null;
1495
+ const config = {
1496
+ token,
1497
+ workspace: repo.owner,
1498
+ repo: repo.repo,
1499
+ branch: ctx.branch ?? "main"
1500
+ };
1501
+ const m = await import("@anymux/bitbucket");
1502
+ const adapters = {
1503
+ "file-system": () => new m.BitbucketFileSystem(config),
1504
+ "git-repo": () => new m.BitbucketGitRepo(config),
1505
+ "git-host": () => new m.BitbucketGitHost(config)
1506
+ };
1507
+ return adapters[capabilityId]();
1508
+ });
1509
+ adapterRegistry.register("gitea", async (token, capabilityId, ctx) => {
1510
+ const creds = JSON.parse(token);
1511
+ const proxyUrl = typeof window !== "undefined" ? window.location.origin : void 0;
1512
+ const config = {
1513
+ url: creds.url,
1514
+ token: creds.token,
1515
+ username: creds.username,
1516
+ password: creds.password,
1517
+ owner: creds.owner,
1518
+ repo: creds.repo,
1519
+ branch: ctx.branch ?? creds.branch ?? "main",
1520
+ proxyUrl
1521
+ };
1522
+ const m = await import("@anymux/gitea");
1523
+ const adapters = {
1524
+ "file-system": () => new m.GiteaFileSystem(config),
1525
+ "git-repo": () => new m.GiteaGitRepo(config),
1526
+ "git-host": () => new m.GiteaGitHost(config)
1527
+ };
1528
+ return adapters[capabilityId]();
1529
+ });
1530
+ adapterRegistry.register("webdav", async (token) => {
1531
+ const creds = JSON.parse(token);
1532
+ const { WebDAVFileSystem } = await import("@anymux/webdav");
1533
+ const proxyUrl = typeof window !== "undefined" ? window.location.origin : void 0;
1534
+ return new WebDAVFileSystem({
1535
+ url: creds.url,
1536
+ username: creds.username,
1537
+ password: creds.password,
1538
+ proxyUrl
1539
+ });
1540
+ });
1541
+ adapterRegistry.register("s3", async (token) => {
1542
+ const creds = JSON.parse(token);
1543
+ const proxyUrl = typeof window !== "undefined" ? window.location.origin : void 0;
1544
+ if (proxyUrl) {
1545
+ const { S3ProxyStorage } = await import("@anymux/s3");
1546
+ const storage$1 = new S3ProxyStorage({
1547
+ proxyUrl,
1548
+ endpoint: creds.endpoint,
1549
+ region: creds.region?.trim(),
1550
+ accessKeyId: creds.accessKeyId?.trim(),
1551
+ secretAccessKey: creds.secretAccessKey?.trim(),
1552
+ forcePathStyle: true
1553
+ });
1554
+ return {
1555
+ storage: storage$1,
1556
+ bucket: creds.bucket || "anymux-test"
1557
+ };
1558
+ }
1559
+ const { S3ObjectStorage } = await import("@anymux/s3");
1560
+ const storage = new S3ObjectStorage({
1561
+ region: creds.region?.trim(),
1562
+ endpoint: creds.endpoint,
1563
+ credentials: {
1564
+ accessKeyId: creds.accessKeyId?.trim(),
1565
+ secretAccessKey: creds.secretAccessKey?.trim()
1566
+ },
1567
+ forcePathStyle: true
1568
+ });
1569
+ return {
1570
+ storage,
1571
+ bucket: creds.bucket || "anymux-test"
1572
+ };
1573
+ });
1574
+ adapterRegistry.register("browser-fs", async () => {
1575
+ const { BrowserFileSystemFactory } = await import("@anymux/browser-fs");
1576
+ const factory = new BrowserFileSystemFactory();
1577
+ const handleInfos = await factory.getAllHandleInfos();
1578
+ if (handleInfos.length > 0) return factory.createFromHandleInfo(handleInfos[0].id);
1579
+ throw new Error("No local file system connected. Please connect first.");
1580
+ });
1581
+ adapterRegistry.register("indexeddb", async () => {
1582
+ const { IndexedDBFileSystem } = await import("@anymux/indexeddb");
1583
+ const fs = new IndexedDBFileSystem();
1584
+ await fs.init();
1585
+ return fs;
1586
+ });
1587
+
1588
+ //#endregion
1589
+ //#region src/components/useAdapter.ts
1590
+ /** Hook to async-create an adapter, run a pre-flight check, and track loading/error state */
1591
+ function useAdapter(factory, deps, preflight) {
1592
+ const [adapter, setAdapter] = useState(null);
1593
+ const [loading, setLoading] = useState(true);
1594
+ const [error, setError] = useState(null);
1595
+ const [retryCount, setRetryCount] = useState(0);
1596
+ useEffect(() => {
1597
+ let cancelled = false;
1598
+ setLoading(true);
1599
+ setError(null);
1600
+ setAdapter(null);
1601
+ factory().then(async (a) => {
1602
+ if (preflight) await preflight(a);
1603
+ if (!cancelled) {
1604
+ setAdapter(a);
1605
+ setLoading(false);
1606
+ }
1607
+ }).catch((e) => {
1608
+ if (!cancelled) {
1609
+ setError(e instanceof Error ? e.message : String(e));
1610
+ setLoading(false);
1611
+ }
1612
+ });
1613
+ return () => {
1614
+ cancelled = true;
1615
+ };
1616
+ }, [...deps, retryCount]);
1617
+ return {
1618
+ adapter,
1619
+ loading,
1620
+ error,
1621
+ retry: () => setRetryCount((c) => c + 1)
1622
+ };
1623
+ }
1624
+
1625
+ //#endregion
1626
+ //#region src/components/CapabilityError.tsx
1627
+ function isAuthError(error) {
1628
+ return /\b(401|403|auth|token|expired|unauthorized|forbidden|denied|scope|permission|sign.?in|log.?in)\b/i.test(error);
1629
+ }
1630
+ /** Render error with contextual actions for capability panels */
1631
+ function CapabilityError({ error, onRetry, onReconnect, onGoBack }) {
1632
+ const authError = isAuthError(error);
1633
+ return /* @__PURE__ */ jsxs("div", {
1634
+ className: "flex flex-col items-center justify-center h-64 gap-4 p-4",
1635
+ children: [/* @__PURE__ */ jsx(ErrorDisplay, {
1636
+ error,
1637
+ title: authError ? "Authentication Required" : "Failed to Load",
1638
+ variant: authError ? "warning" : "destructive",
1639
+ className: "max-w-md w-full",
1640
+ ...onRetry != null ? { onRetry } : {}
1641
+ }), /* @__PURE__ */ jsxs("div", {
1642
+ className: "flex items-center gap-2",
1643
+ children: [onReconnect && /* @__PURE__ */ jsxs("button", {
1644
+ onClick: onReconnect,
1645
+ className: "inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-md bg-primary text-primary-foreground hover:bg-primary/90 transition-colors",
1646
+ children: [/* @__PURE__ */ jsx(RefreshCw, { className: "h-3 w-3" }), "Reconnect"]
1647
+ }), onGoBack && /* @__PURE__ */ jsxs("button", {
1648
+ onClick: onGoBack,
1649
+ className: "inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium rounded-md bg-muted hover:bg-muted/80 transition-colors",
1650
+ children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-3 w-3" }), "Back to Dashboard"]
1651
+ })]
1652
+ })]
1653
+ });
1654
+ }
1655
+
1656
+ //#endregion
1657
+ //#region src/components/CapabilityPanel.tsx
1658
+ const FileBrowser = React.lazy(() => import("@anymux/fs-ui").then((m) => ({ default: m.FileBrowser })));
1659
+ const LazyMediaBrowser = React.lazy(() => import("@anymux/object-ui").then((m) => ({ default: ({ provider }) => {
1660
+ const model = new m.MediaBrowserModel(provider);
1661
+ return React.createElement(m.MediaBrowser, {
1662
+ model,
1663
+ provider,
1664
+ className: "h-full"
1665
+ });
1666
+ } })));
1667
+ const LazyContactBrowser = React.lazy(() => import("@anymux/object-ui").then((m) => ({ default: ({ provider }) => {
1668
+ const model = new m.ContactListModel(provider);
1669
+ return React.createElement(m.ContactBrowser, {
1670
+ model,
1671
+ className: "h-full"
1672
+ });
1673
+ } })));
1674
+ const LazyCalendarBrowser = React.lazy(() => import("@anymux/object-ui").then((m) => ({ default: ({ provider }) => {
1675
+ const model = new m.CalendarModel(provider);
1676
+ return React.createElement(m.CalendarBrowser, {
1677
+ model,
1678
+ className: "h-full"
1679
+ });
1680
+ } })));
1681
+ const RepoPicker$1 = React.lazy(() => import("./RepoPicker-BprFGOn7.js").then((m) => ({ default: m.RepoPicker })));
1682
+ const ObjectStorageBrowserLazy = React.lazy(() => import("./ObjectStorageBrowser-B2YkUxMl.js").then((m) => ({ default: m.ObjectStorageBrowser })));
1683
+ const GitRepoBrowserLazy = React.lazy(() => import("./GitBrowser-CIyWiuX-.js").then((m) => ({ default: m.GitRepoBrowser })));
1684
+ const GitHostBrowserLazy = React.lazy(() => import("./GitBrowser-CIyWiuX-.js").then((m) => ({ default: m.GitHostBrowser })));
1685
+ const CAPABILITY_LABELS = {
1686
+ "file-system": "File System",
1687
+ "object-storage": "Object Storage",
1688
+ "git-repo": "Git",
1689
+ "git-host": "Git",
1690
+ "media": "Media",
1691
+ "contacts": "Contacts",
1692
+ "calendar": "Calendar"
1693
+ };
1694
+ const REPO_SERVICES$1 = new Set([
1695
+ "github",
1696
+ "gitlab",
1697
+ "bitbucket",
1698
+ "gitea"
1699
+ ]);
1700
+ const LoadingFallback = () => /* @__PURE__ */ jsx("div", {
1701
+ className: "flex items-center justify-center h-64",
1702
+ children: /* @__PURE__ */ jsx(LoadingSpinner, { label: "Loading..." })
1703
+ });
1704
+ function GitRepoBrowserContent({ serviceId, token, gitRepo, owner, repo, selectedRepo, onError, actionNotifications }) {
1705
+ const createFileSystem = useCallback(async (branch) => {
1706
+ const context = {
1707
+ selectedRepo,
1708
+ branch
1709
+ };
1710
+ const fs = await adapterRegistry.create(serviceId, token, "file-system", context);
1711
+ return fs;
1712
+ }, [
1713
+ serviceId,
1714
+ token,
1715
+ selectedRepo
1716
+ ]);
1717
+ const [gitHost, setGitHost] = useState();
1718
+ React.useEffect(() => {
1719
+ let cancelled = false;
1720
+ adapterRegistry.create(serviceId, token, "git-host", { selectedRepo }).then((adapter) => {
1721
+ if (!cancelled) setGitHost(adapter);
1722
+ }).catch(() => {});
1723
+ return () => {
1724
+ cancelled = true;
1725
+ };
1726
+ }, [
1727
+ serviceId,
1728
+ token,
1729
+ selectedRepo
1730
+ ]);
1731
+ return /* @__PURE__ */ jsx(GitRepoBrowserLazy, {
1732
+ gitRepo,
1733
+ owner,
1734
+ repo,
1735
+ createFileSystem,
1736
+ gitHost,
1737
+ onError,
1738
+ actionNotifications
1739
+ });
1740
+ }
1741
+ function GitHostBrowserWithRepo({ serviceId, token, gitHost, owner, repo, selectedRepo, onError, actionNotifications }) {
1742
+ const [gitRepo, setGitRepo] = useState();
1743
+ const [repoLoaded, setRepoLoaded] = useState(false);
1744
+ React.useEffect(() => {
1745
+ let cancelled = false;
1746
+ adapterRegistry.create(serviceId, token, "git-repo", { selectedRepo }).then((adapter) => {
1747
+ if (!cancelled) setGitRepo(adapter);
1748
+ }).catch(() => {}).finally(() => {
1749
+ if (!cancelled) setRepoLoaded(true);
1750
+ });
1751
+ return () => {
1752
+ cancelled = true;
1753
+ };
1754
+ }, [
1755
+ serviceId,
1756
+ token,
1757
+ selectedRepo
1758
+ ]);
1759
+ const createFileSystem = useCallback(async (branch) => {
1760
+ const context = {
1761
+ selectedRepo,
1762
+ branch
1763
+ };
1764
+ const fs = await adapterRegistry.create(serviceId, token, "file-system", context);
1765
+ return fs;
1766
+ }, [
1767
+ serviceId,
1768
+ token,
1769
+ selectedRepo
1770
+ ]);
1771
+ if (!repoLoaded) return /* @__PURE__ */ jsx(LoadingFallback, {});
1772
+ if (gitRepo) return /* @__PURE__ */ jsx(GitRepoBrowserLazy, {
1773
+ gitRepo,
1774
+ owner,
1775
+ repo,
1776
+ createFileSystem,
1777
+ gitHost,
1778
+ onError,
1779
+ actionNotifications
1780
+ });
1781
+ return /* @__PURE__ */ jsx(GitHostBrowserLazy, { gitHost });
1782
+ }
1783
+ const GenericCapabilityContent = observer(function GenericCapabilityContent$1({ serviceId, capabilityId, token, dashboardModel, connectionManager }) {
1784
+ const selectedRepo = dashboardModel.getSelectedRepo(serviceId);
1785
+ const context = { selectedRepo: selectedRepo ?? void 0 };
1786
+ const { adapter, loading, error, retry } = useAdapter(
1787
+ async () => {
1788
+ console.info(`[AnyMux] CapabilityPanel: refreshing token for ${serviceId}/${capabilityId}`);
1789
+ const freshToken = await connectionManager.refreshToken(serviceId) ?? token;
1790
+ console.info(`[AnyMux] CapabilityPanel: creating adapter with token ${freshToken.slice(0, 8)}…${freshToken.slice(-4)}`);
1791
+ const a = adapterRegistry.create(serviceId, freshToken, capabilityId, context);
1792
+ return a;
1793
+ },
1794
+ [
1795
+ serviceId,
1796
+ capabilityId,
1797
+ token,
1798
+ selectedRepo?.owner,
1799
+ selectedRepo?.repo
1800
+ ],
1801
+ // Pre-flight: verify the adapter actually works before mounting the browser UI.
1802
+ // If we get a 401, try one more refresh — better-auth may not have detected
1803
+ // the token as expired (e.g. missing accessTokenExpiresAt in the DB).
1804
+ async (a) => {
1805
+ const runCheck = async (adapter$1) => {
1806
+ if (capabilityId === "file-system") await adapter$1.readdir("/");
1807
+ else if (capabilityId === "git-host") {
1808
+ if (adapter$1.listRepositories) await adapter$1.listRepositories({
1809
+ page: 1,
1810
+ perPage: 1
1811
+ });
1812
+ } else if (capabilityId === "git-repo") {
1813
+ if (adapter$1.listBranches) await adapter$1.listBranches();
1814
+ } else if (capabilityId === "calendar") {
1815
+ if (adapter$1.getCalendars) await adapter$1.getCalendars();
1816
+ } else if (capabilityId === "contacts") {
1817
+ if (adapter$1.getContacts) await adapter$1.getContacts({ limit: 1 });
1818
+ } else if (capabilityId === "media") {
1819
+ if (adapter$1.getAlbums) await adapter$1.getAlbums();
1820
+ }
1821
+ };
1822
+ try {
1823
+ console.info(`[AnyMux] CapabilityPanel: running pre-flight for ${serviceId}/${capabilityId}`);
1824
+ await runCheck(a);
1825
+ console.info(`[AnyMux] CapabilityPanel: pre-flight passed`);
1826
+ } catch (err) {
1827
+ console.warn(`[AnyMux] CapabilityPanel: pre-flight failed for ${serviceId}/${capabilityId}:`, err instanceof Error ? err.message : err);
1828
+ if (err instanceof Error && isAuthError(err.message)) {
1829
+ console.info(`[AnyMux] CapabilityPanel: auth error detected, attempting second refresh`);
1830
+ const retryToken = await connectionManager.refreshToken(serviceId);
1831
+ if (retryToken && retryToken !== token) {
1832
+ console.info(`[AnyMux] CapabilityPanel: got different token on retry, rebuilding adapter`);
1833
+ const retryAdapter = await adapterRegistry.create(serviceId, retryToken, capabilityId, context);
1834
+ await runCheck(retryAdapter);
1835
+ Object.assign(a, retryAdapter);
1836
+ return;
1837
+ }
1838
+ console.warn(`[AnyMux] CapabilityPanel: retry token same as original, giving up`);
1839
+ }
1840
+ throw err;
1841
+ }
1842
+ }
1843
+ );
1844
+ const handleReconnect = async () => {
1845
+ const service = dashboardModel.selectedService;
1846
+ await connectionManager.disconnect(serviceId);
1847
+ dashboardModel.closePanel();
1848
+ const isOAuth = service && ![
1849
+ "s3",
1850
+ "webdav",
1851
+ "gitea",
1852
+ "icloud",
1853
+ "browser-fs",
1854
+ "indexeddb"
1855
+ ].includes(service.authProvider);
1856
+ if (isOAuth) await connectionManager.connect(serviceId);
1857
+ else connectionManager.requestReconnect(serviceId);
1858
+ };
1859
+ const handleGoBack = () => {
1860
+ dashboardModel.closePanel();
1861
+ };
1862
+ const [runtimeError, setRuntimeError] = useState(null);
1863
+ const handleRuntimeError = (err) => {
1864
+ if (isAuthError(err.message)) setRuntimeError(err.message);
1865
+ };
1866
+ const handleNotify = useCallback((type, message) => {
1867
+ if (type === "success") showActionToast(dashboardModel.actionNotifications, "create", message);
1868
+ else showErrorToast(message);
1869
+ }, [dashboardModel.actionNotifications]);
1870
+ const handleAction = useCallback((action) => {
1871
+ showActionToast(dashboardModel.actionNotifications, action.type, action.message, { undo: action.undo });
1872
+ }, [dashboardModel.actionNotifications]);
1873
+ if (loading) return /* @__PURE__ */ jsx(LoadingFallback, {});
1874
+ if (error) return /* @__PURE__ */ jsx(CapabilityError, {
1875
+ error,
1876
+ onRetry: retry,
1877
+ onReconnect: handleReconnect,
1878
+ onGoBack: handleGoBack
1879
+ });
1880
+ if (runtimeError) return /* @__PURE__ */ jsx(CapabilityError, {
1881
+ error: runtimeError,
1882
+ onReconnect: handleReconnect,
1883
+ onGoBack: handleGoBack
1884
+ });
1885
+ if (adapter === null) return null;
1886
+ return match(capabilityId).with("file-system", () => /* @__PURE__ */ jsx(Suspense, {
1887
+ fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
1888
+ children: /* @__PURE__ */ jsx(FileBrowser, {
1889
+ fileSystem: adapter,
1890
+ className: "h-full",
1891
+ initialPath: dashboardModel.browserPath !== "/" ? dashboardModel.browserPath : void 0,
1892
+ onPathChange: (path) => dashboardModel.setBrowserPath(path),
1893
+ onError: handleRuntimeError,
1894
+ onNotify: handleNotify,
1895
+ onAction: handleAction,
1896
+ showBreadcrumbs: false
1897
+ })
1898
+ })).with("object-storage", () => {
1899
+ const s3Result = adapter;
1900
+ return /* @__PURE__ */ jsx(Suspense, {
1901
+ fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
1902
+ children: /* @__PURE__ */ jsx(ObjectStorageBrowserLazy, {
1903
+ storage: s3Result.storage,
1904
+ bucket: s3Result.bucket
1905
+ })
1906
+ });
1907
+ }).with("git-repo", () => {
1908
+ const repo = selectedRepo ?? {
1909
+ owner: "",
1910
+ repo: ""
1911
+ };
1912
+ return /* @__PURE__ */ jsx(Suspense, {
1913
+ fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
1914
+ children: /* @__PURE__ */ jsx(GitRepoBrowserContent, {
1915
+ serviceId,
1916
+ token,
1917
+ gitRepo: adapter,
1918
+ owner: repo.owner,
1919
+ repo: repo.repo,
1920
+ selectedRepo: selectedRepo ?? void 0,
1921
+ onError: handleRuntimeError,
1922
+ actionNotifications: dashboardModel.actionNotifications
1923
+ })
1924
+ });
1925
+ }).with("git-host", () => {
1926
+ const repo = selectedRepo ?? {
1927
+ owner: "",
1928
+ repo: ""
1929
+ };
1930
+ return /* @__PURE__ */ jsx(Suspense, {
1931
+ fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
1932
+ children: /* @__PURE__ */ jsx(GitHostBrowserWithRepo, {
1933
+ serviceId,
1934
+ token,
1935
+ gitHost: adapter,
1936
+ owner: repo.owner,
1937
+ repo: repo.repo,
1938
+ selectedRepo: selectedRepo ?? void 0,
1939
+ onError: handleRuntimeError,
1940
+ actionNotifications: dashboardModel.actionNotifications
1941
+ })
1942
+ });
1943
+ }).with("media", () => /* @__PURE__ */ jsx(Suspense, {
1944
+ fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
1945
+ children: /* @__PURE__ */ jsx(LazyMediaBrowser, { provider: adapter })
1946
+ })).with("contacts", () => /* @__PURE__ */ jsx(Suspense, {
1947
+ fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
1948
+ children: /* @__PURE__ */ jsx(LazyContactBrowser, { provider: adapter })
1949
+ })).with("calendar", () => /* @__PURE__ */ jsx(Suspense, {
1950
+ fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
1951
+ children: /* @__PURE__ */ jsx(LazyCalendarBrowser, { provider: adapter })
1952
+ })).exhaustive();
1953
+ });
1954
+ const CapabilityContent = observer(({ serviceId, capabilityId, connectionManager, dashboardModel }) => {
1955
+ const service = dashboardModel.selectedService;
1956
+ if (!service) return null;
1957
+ const token = connectionManager.getToken(serviceId);
1958
+ if (!token) return /* @__PURE__ */ jsx(CapabilityError, { error: "Not connected. Please connect the service first." });
1959
+ if (REPO_SERVICES$1.has(serviceId) && (capabilityId === "file-system" || capabilityId === "git-repo" || capabilityId === "git-host")) {
1960
+ let selectedRepo = dashboardModel.getSelectedRepo(serviceId);
1961
+ if (!selectedRepo) try {
1962
+ const creds = JSON.parse(token);
1963
+ if (creds.owner && creds.repo) {
1964
+ dashboardModel.setSelectedRepo(serviceId, {
1965
+ owner: creds.owner,
1966
+ repo: creds.repo
1967
+ });
1968
+ selectedRepo = {
1969
+ owner: creds.owner,
1970
+ repo: creds.repo
1971
+ };
1972
+ }
1973
+ } catch {}
1974
+ if (!selectedRepo) return /* @__PURE__ */ jsx(Suspense, {
1975
+ fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
1976
+ children: /* @__PURE__ */ jsx(RepoPicker$1, {
1977
+ serviceId,
1978
+ accessToken: token,
1979
+ onSelectRepo: (repo) => dashboardModel.setSelectedRepo(serviceId, repo)
1980
+ })
1981
+ });
1982
+ }
1983
+ return /* @__PURE__ */ jsx(GenericCapabilityContent, {
1984
+ serviceId,
1985
+ capabilityId,
1986
+ token,
1987
+ dashboardModel,
1988
+ connectionManager
1989
+ });
1990
+ });
1991
+ const ScopeWarningBanner = observer(({ serviceId, capabilityId, connectionManager }) => {
1992
+ const token = connectionManager.getToken(serviceId);
1993
+ if (!token) return null;
1994
+ const hasScopes = connectionManager.hasCapabilityScopes(serviceId, capabilityId);
1995
+ if (hasScopes) return null;
1996
+ return /* @__PURE__ */ jsxs("div", {
1997
+ className: "flex items-center gap-2 px-4 py-2 bg-yellow-50 dark:bg-yellow-900/20 border-b border-yellow-200 dark:border-yellow-800",
1998
+ children: [/* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4 text-yellow-500 flex-shrink-0" }), /* @__PURE__ */ jsx("span", {
1999
+ className: "text-xs text-yellow-700 dark:text-yellow-400",
2000
+ children: "Required scopes not granted. Some features may not work. Try reconnecting with the needed permissions."
2001
+ })]
2002
+ });
2003
+ });
2004
+ const CapabilityPanel = observer(({ dashboardModel, connectionManager }) => {
2005
+ if (!dashboardModel.panelOpen || !dashboardModel.selectedCell) return null;
2006
+ const { serviceId, capabilityId } = dashboardModel.selectedCell;
2007
+ const service = dashboardModel.selectedService;
2008
+ const selectedRepo = dashboardModel.getSelectedRepo(serviceId);
2009
+ const hasRepo = REPO_SERVICES$1.has(serviceId) && selectedRepo;
2010
+ return /* @__PURE__ */ jsxs("div", {
2011
+ className: "border border-border rounded-lg mt-4 overflow-hidden bg-card flex flex-col h-[70vh]",
2012
+ children: [
2013
+ /* @__PURE__ */ jsxs("div", {
2014
+ className: "flex items-center justify-between px-4 py-2 bg-muted border-b border-border",
2015
+ children: [/* @__PURE__ */ jsxs("div", {
2016
+ className: "flex items-center gap-2",
2017
+ children: [/* @__PURE__ */ jsxs("span", {
2018
+ className: "text-sm font-medium truncate",
2019
+ title: `${service?.name} — ${CAPABILITY_LABELS[capabilityId]}`,
2020
+ children: [
2021
+ service?.name,
2022
+ " — ",
2023
+ CAPABILITY_LABELS[capabilityId]
2024
+ ]
2025
+ }), hasRepo && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("span", {
2026
+ className: "text-xs text-muted-foreground truncate",
2027
+ title: `${selectedRepo.owner}/${selectedRepo.repo}`,
2028
+ children: [
2029
+ "(",
2030
+ selectedRepo.owner,
2031
+ "/",
2032
+ selectedRepo.repo,
2033
+ ")"
2034
+ ]
2035
+ }), /* @__PURE__ */ jsxs("button", {
2036
+ onClick: () => dashboardModel.clearSelectedRepo(serviceId),
2037
+ className: "inline-flex items-center gap-1 px-2 py-0.5 text-[11px] font-medium rounded bg-muted hover:bg-muted/80 transition-colors",
2038
+ children: [/* @__PURE__ */ jsx(ArrowLeft, { className: "h-3 w-3" }), "Change repo"]
2039
+ })] })]
2040
+ }), /* @__PURE__ */ jsx("button", {
2041
+ onClick: () => dashboardModel.closePanel(),
2042
+ className: "p-1 rounded hover:bg-muted transition-colors",
2043
+ children: /* @__PURE__ */ jsx(X, { className: "h-4 w-4" })
2044
+ })]
2045
+ }),
2046
+ /* @__PURE__ */ jsx(ScopeWarningBanner, {
2047
+ serviceId,
2048
+ capabilityId,
2049
+ connectionManager
2050
+ }),
2051
+ /* @__PURE__ */ jsx("div", {
2052
+ className: "flex-1 min-h-0 overflow-hidden",
2053
+ children: /* @__PURE__ */ jsx(Suspense, {
2054
+ fallback: /* @__PURE__ */ jsx(LoadingFallback, {}),
2055
+ children: /* @__PURE__ */ jsx(CapabilityContent, {
2056
+ serviceId,
2057
+ capabilityId,
2058
+ connectionManager,
2059
+ dashboardModel
2060
+ })
2061
+ })
2062
+ })
2063
+ ]
2064
+ });
2065
+ });
2066
+
2067
+ //#endregion
2068
+ //#region src/components/ActionHistoryPanel.tsx
2069
+ const ACTION_ICONS = {
2070
+ delete: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5 text-destructive" }),
2071
+ rename: /* @__PURE__ */ jsx(FileEdit, { className: "h-3.5 w-3.5 text-blue-500" }),
2072
+ create: /* @__PURE__ */ jsx(FilePlus, { className: "h-3.5 w-3.5 text-green-500" }),
2073
+ upload: /* @__PURE__ */ jsx(Upload, { className: "h-3.5 w-3.5 text-purple-500" }),
2074
+ copy: /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5 text-muted-foreground" }),
2075
+ move: /* @__PURE__ */ jsx(Move, { className: "h-3.5 w-3.5 text-orange-500" })
2076
+ };
2077
+ function formatTimeAgo(date) {
2078
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1e3);
2079
+ if (seconds < 60) return "just now";
2080
+ const minutes = Math.floor(seconds / 60);
2081
+ if (minutes < 60) return `${minutes}m ago`;
2082
+ const hours = Math.floor(minutes / 60);
2083
+ if (hours < 24) return `${hours}h ago`;
2084
+ return `${Math.floor(hours / 24)}d ago`;
2085
+ }
2086
+ const ActionItem = observer(({ action, model }) => {
2087
+ const [undoing, setUndoing] = useState(false);
2088
+ const handleUndo = async () => {
2089
+ if (!action.undo || action.undone || undoing) return;
2090
+ setUndoing(true);
2091
+ try {
2092
+ await action.undo();
2093
+ model.markUndone(action.id);
2094
+ } catch {} finally {
2095
+ setUndoing(false);
2096
+ }
2097
+ };
2098
+ return /* @__PURE__ */ jsxs("div", {
2099
+ className: `flex items-start gap-2 px-3 py-2 text-xs ${action.undone ? "opacity-50" : ""}`,
2100
+ children: [
2101
+ /* @__PURE__ */ jsx("span", {
2102
+ className: "mt-0.5 flex-shrink-0",
2103
+ children: ACTION_ICONS[action.type]
2104
+ }),
2105
+ /* @__PURE__ */ jsxs("div", {
2106
+ className: "flex-1 min-w-0",
2107
+ children: [/* @__PURE__ */ jsx("p", {
2108
+ className: `text-foreground truncate ${action.undone ? "line-through" : ""}`,
2109
+ title: action.description,
2110
+ children: action.description
2111
+ }), /* @__PURE__ */ jsx("p", {
2112
+ className: "text-muted-foreground mt-0.5",
2113
+ children: formatTimeAgo(action.timestamp)
2114
+ })]
2115
+ }),
2116
+ action.undo && !action.undone && /* @__PURE__ */ jsx("button", {
2117
+ onClick: handleUndo,
2118
+ disabled: undoing,
2119
+ className: "flex-shrink-0 p-1 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors disabled:opacity-50",
2120
+ title: "Undo",
2121
+ children: /* @__PURE__ */ jsx(Undo2, { className: "h-3.5 w-3.5" })
2122
+ })
2123
+ ]
2124
+ });
2125
+ });
2126
+ const ActionHistoryPanel = observer(({ model }) => {
2127
+ const [open, setOpen] = useState(false);
2128
+ const panelRef = useRef(null);
2129
+ const actions = model.recentActions;
2130
+ useEffect(() => {
2131
+ if (!open) return;
2132
+ const handleClick = (e) => {
2133
+ if (panelRef.current && !panelRef.current.contains(e.target)) setOpen(false);
2134
+ };
2135
+ document.addEventListener("mousedown", handleClick);
2136
+ return () => document.removeEventListener("mousedown", handleClick);
2137
+ }, [open]);
2138
+ return /* @__PURE__ */ jsxs("div", {
2139
+ className: "relative",
2140
+ ref: panelRef,
2141
+ children: [/* @__PURE__ */ jsxs("button", {
2142
+ onClick: () => setOpen(!open),
2143
+ className: "relative p-1.5 rounded-md hover:bg-background/80 text-muted-foreground hover:text-foreground transition-colors",
2144
+ title: "Action history",
2145
+ children: [/* @__PURE__ */ jsx(History, { className: "h-4 w-4" }), actions.length > 0 && /* @__PURE__ */ jsx("span", {
2146
+ className: "absolute -top-0.5 -right-0.5 h-3.5 w-3.5 rounded-full bg-primary text-primary-foreground text-[9px] font-medium flex items-center justify-center",
2147
+ children: actions.length > 9 ? "9+" : actions.length
2148
+ })]
2149
+ }), open && /* @__PURE__ */ jsxs("div", {
2150
+ className: "absolute right-0 top-full mt-1 w-72 rounded-lg border border-border bg-card shadow-lg z-50 animate-in fade-in slide-in-from-top-1 duration-150",
2151
+ children: [/* @__PURE__ */ jsxs("div", {
2152
+ className: "flex items-center justify-between px-3 py-2 border-b border-border",
2153
+ children: [/* @__PURE__ */ jsx("span", {
2154
+ className: "text-xs font-medium text-foreground",
2155
+ children: "Recent Actions"
2156
+ }), /* @__PURE__ */ jsxs("div", {
2157
+ className: "flex items-center gap-1",
2158
+ children: [actions.length > 0 && /* @__PURE__ */ jsx("button", {
2159
+ onClick: () => model.clear(),
2160
+ className: "text-[10px] text-muted-foreground hover:text-foreground px-1.5 py-0.5 rounded hover:bg-muted transition-colors",
2161
+ children: "Clear"
2162
+ }), /* @__PURE__ */ jsx("button", {
2163
+ onClick: () => setOpen(false),
2164
+ className: "p-0.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground transition-colors",
2165
+ children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
2166
+ })]
2167
+ })]
2168
+ }), /* @__PURE__ */ jsx("div", {
2169
+ className: "max-h-64 overflow-y-auto divide-y divide-border",
2170
+ children: actions.length === 0 ? /* @__PURE__ */ jsx("div", {
2171
+ className: "px-3 py-6 text-center text-xs text-muted-foreground",
2172
+ children: "No actions yet"
2173
+ }) : actions.map((action) => /* @__PURE__ */ jsx(ActionItem, {
2174
+ action,
2175
+ model
2176
+ }, action.id))
2177
+ })]
2178
+ })]
2179
+ });
2180
+ });
2181
+
2182
+ //#endregion
2183
+ //#region src/components/FullScreenBrowser.tsx
2184
+ const REPO_SERVICES = new Set([
2185
+ "github",
2186
+ "gitlab",
2187
+ "bitbucket",
2188
+ "gitea"
2189
+ ]);
2190
+ const FullScreenBrowser = observer(({ dashboardModel, connectionManager }) => {
2191
+ const cell = dashboardModel.selectedCell;
2192
+ if (!cell) return null;
2193
+ const { serviceId, capabilityId } = cell;
2194
+ const service = dashboardModel.selectedService;
2195
+ if (!service) return null;
2196
+ const selectedRepo = dashboardModel.getSelectedRepo(serviceId);
2197
+ const hasRepo = REPO_SERVICES.has(serviceId) && selectedRepo;
2198
+ const browserPath = dashboardModel.browserPath;
2199
+ return /* @__PURE__ */ jsxs("div", {
2200
+ className: "flex flex-col h-full flex-1 min-h-0",
2201
+ children: [
2202
+ /* @__PURE__ */ jsxs("div", {
2203
+ className: "flex items-center gap-1 px-2 sm:px-4 py-2 bg-muted border-b border-border text-xs sm:text-sm flex-shrink-0 min-h-[40px]",
2204
+ children: [/* @__PURE__ */ jsxs("div", {
2205
+ className: "flex items-center gap-1 flex-1 min-w-0 flex-wrap",
2206
+ children: [/* @__PURE__ */ jsx(PathBreadcrumb, {
2207
+ path: browserPath,
2208
+ onNavigate: (path) => dashboardModel.setBrowserPath(path),
2209
+ showHome: false,
2210
+ editable: true,
2211
+ prefixSegments: [
2212
+ {
2213
+ label: "Dashboard",
2214
+ onClick: () => dashboardModel.closePanel()
2215
+ },
2216
+ {
2217
+ label: service.name,
2218
+ onClick: () => dashboardModel.closePanel()
2219
+ },
2220
+ {
2221
+ label: CAPABILITY_LABELS[capabilityId],
2222
+ onClick: () => dashboardModel.setBrowserPath("/")
2223
+ },
2224
+ ...hasRepo ? [{ label: `${selectedRepo.owner}/${selectedRepo.repo}` }] : []
2225
+ ]
2226
+ }), hasRepo && /* @__PURE__ */ jsxs("button", {
2227
+ onClick: () => dashboardModel.clearSelectedRepo(serviceId),
2228
+ className: "ml-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-muted hover:bg-muted/80 transition-colors",
2229
+ children: [/* @__PURE__ */ jsx("span", {
2230
+ className: "hidden sm:inline",
2231
+ children: "Change"
2232
+ }), /* @__PURE__ */ jsx(ArrowLeft, { className: "h-3 w-3 sm:hidden" })]
2233
+ })]
2234
+ }), /* @__PURE__ */ jsx(ActionHistoryPanel, { model: dashboardModel.actionNotifications })]
2235
+ }),
2236
+ /* @__PURE__ */ jsx(ScopeWarningBanner, {
2237
+ serviceId,
2238
+ capabilityId,
2239
+ connectionManager
2240
+ }),
2241
+ /* @__PURE__ */ jsx("div", {
2242
+ className: "flex-1 min-h-0 overflow-hidden",
2243
+ children: /* @__PURE__ */ jsx(Suspense, {
2244
+ fallback: /* @__PURE__ */ jsx("div", {
2245
+ className: "flex items-center justify-center h-64",
2246
+ children: /* @__PURE__ */ jsx(LoadingSpinner, { label: "Loading..." })
2247
+ }),
2248
+ children: /* @__PURE__ */ jsx(CapabilityContent, {
2249
+ serviceId,
2250
+ capabilityId,
2251
+ connectionManager,
2252
+ dashboardModel
2253
+ })
2254
+ })
2255
+ })
2256
+ ]
2257
+ });
2258
+ });
2259
+
2260
+ //#endregion
2261
+ //#region src/components/ServiceDashboard.tsx
2262
+ const COLUMN_HEADERS = [
2263
+ {
2264
+ label: "Service",
2265
+ className: "text-left"
2266
+ },
2267
+ {
2268
+ label: "Status",
2269
+ className: "text-left"
2270
+ },
2271
+ {
2272
+ label: "FS",
2273
+ className: "text-center"
2274
+ },
2275
+ {
2276
+ label: "Obj Storage",
2277
+ className: "text-center"
2278
+ },
2279
+ {
2280
+ label: "Git",
2281
+ className: "text-center"
2282
+ },
2283
+ {
2284
+ label: "Media",
2285
+ className: "text-center"
2286
+ },
2287
+ {
2288
+ label: "Contacts",
2289
+ className: "text-center"
2290
+ },
2291
+ {
2292
+ label: "Calendar",
2293
+ className: "text-center"
2294
+ }
2295
+ ];
2296
+ const ServiceDashboard = observer(({ connectionManager, dashboardModel }) => {
2297
+ const services = serviceRegistry.getAll();
2298
+ if (dashboardModel.panelOpen && dashboardModel.selectedCell) return /* @__PURE__ */ jsx("div", {
2299
+ className: "animate-in fade-in duration-200 h-full flex flex-col min-h-0",
2300
+ children: /* @__PURE__ */ jsx(FullScreenBrowser, {
2301
+ dashboardModel,
2302
+ connectionManager
2303
+ })
2304
+ });
2305
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("div", {
2306
+ className: "hidden md:block",
2307
+ children: /* @__PURE__ */ jsx("div", {
2308
+ className: "overflow-x-auto rounded-lg border border-border",
2309
+ children: /* @__PURE__ */ jsxs("table", {
2310
+ className: "w-full text-sm",
2311
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", {
2312
+ className: "bg-muted border-b border-border",
2313
+ children: COLUMN_HEADERS.map((col) => /* @__PURE__ */ jsx("th", {
2314
+ className: `px-3 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider ${col.className}`,
2315
+ children: col.label
2316
+ }, col.label))
2317
+ }) }), /* @__PURE__ */ jsx("tbody", {
2318
+ className: "bg-card divide-y divide-border",
2319
+ children: services.map((service) => /* @__PURE__ */ jsx(ServiceRow, {
2320
+ service,
2321
+ connectionManager,
2322
+ dashboardModel
2323
+ }, service.id))
2324
+ })]
2325
+ })
2326
+ })
2327
+ }), /* @__PURE__ */ jsx("div", {
2328
+ className: "md:hidden space-y-3",
2329
+ children: services.map((service) => /* @__PURE__ */ jsx(ServiceCard, {
2330
+ service,
2331
+ connectionManager,
2332
+ dashboardModel
2333
+ }, service.id))
2334
+ })] });
2335
+ });
2336
+
2337
+ //#endregion
2338
+ //#region src/components/ConnectionStatus.tsx
2339
+ const ConnectionStatusIndicator = ({ status }) => {
2340
+ const config = match(status).with("disconnected", () => ({
2341
+ status: "neutral",
2342
+ label: "Disconnected"
2343
+ })).with("connecting", () => ({
2344
+ status: "warning",
2345
+ label: "Connecting...",
2346
+ pulse: true
2347
+ })).with("connected", () => ({
2348
+ status: "success",
2349
+ label: "Connected"
2350
+ })).with("expired", () => ({
2351
+ status: "warning",
2352
+ label: "Expired"
2353
+ })).with("error", () => ({
2354
+ status: "error",
2355
+ label: "Error"
2356
+ })).with("not_configured", () => ({
2357
+ status: "neutral",
2358
+ label: "Not Configured"
2359
+ })).with("loading", () => ({
2360
+ status: "neutral",
2361
+ label: "Loading..."
2362
+ })).exhaustive();
2363
+ return /* @__PURE__ */ jsx(StatusIndicator, {
2364
+ status: config.status,
2365
+ label: config.label,
2366
+ pulse: config.status === "warning" && status === "connecting"
2367
+ });
2368
+ };
2369
+
2370
+ //#endregion
2371
+ //#region src/components/GitHubRepoPicker.tsx
2372
+ function GitHubRepoPicker({ accessToken, onSelectRepo }) {
2373
+ const [repos, setRepos] = useState([]);
2374
+ const [loading, setLoading] = useState(true);
2375
+ const [error, setError] = useState(null);
2376
+ const [search, setSearch] = useState("");
2377
+ useEffect(() => {
2378
+ let cancelled = false;
2379
+ async function fetchRepos() {
2380
+ setLoading(true);
2381
+ setError(null);
2382
+ try {
2383
+ const res = await fetch("https://api.github.com/user/repos?sort=updated&per_page=50", { headers: { Authorization: `Bearer ${accessToken}` } });
2384
+ if (!res.ok) throw new Error(`GitHub API error: ${res.status}`);
2385
+ const data = await res.json();
2386
+ if (!cancelled) setRepos(data);
2387
+ } catch (err) {
2388
+ if (!cancelled) setError(err instanceof Error ? err.message : "Failed to fetch repos");
2389
+ } finally {
2390
+ if (!cancelled) setLoading(false);
2391
+ }
2392
+ }
2393
+ fetchRepos();
2394
+ return () => {
2395
+ cancelled = true;
2396
+ };
2397
+ }, [accessToken]);
2398
+ const filtered = useMemo(() => repos.filter((r) => r.full_name.toLowerCase().includes(search.toLowerCase())), [repos, search]);
2399
+ if (loading) return /* @__PURE__ */ jsxs("div", {
2400
+ className: "flex items-center justify-center py-8 text-gray-400",
2401
+ children: [/* @__PURE__ */ jsx(Loader2, { className: "h-5 w-5 animate-spin mr-2" }), "Loading repositories..."]
2402
+ });
2403
+ if (error) return /* @__PURE__ */ jsx("div", {
2404
+ className: "rounded-lg border border-red-800 bg-red-950/50 p-4 text-red-300 text-sm",
2405
+ children: error
2406
+ });
2407
+ return /* @__PURE__ */ jsxs("div", {
2408
+ className: "flex flex-col gap-2",
2409
+ children: [/* @__PURE__ */ jsxs("div", {
2410
+ className: "relative",
2411
+ children: [/* @__PURE__ */ jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-500" }), /* @__PURE__ */ jsx("input", {
2412
+ type: "text",
2413
+ placeholder: "Search repositories...",
2414
+ value: search,
2415
+ onChange: (e) => setSearch(e.target.value),
2416
+ className: "w-full rounded-md border border-gray-700 bg-gray-900 py-2 pl-9 pr-3 text-sm text-gray-200 placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
2417
+ })]
2418
+ }), /* @__PURE__ */ jsx("div", {
2419
+ className: "max-h-64 overflow-y-auto rounded-md border border-gray-700",
2420
+ children: filtered.length === 0 ? /* @__PURE__ */ jsx("div", {
2421
+ className: "px-4 py-6 text-center text-sm text-gray-500",
2422
+ children: "No repositories found"
2423
+ }) : filtered.map((repo) => {
2424
+ const parts = repo.full_name.split("/");
2425
+ const owner = parts[0] ?? "";
2426
+ const name = parts[1] ?? "";
2427
+ return /* @__PURE__ */ jsxs("button", {
2428
+ type: "button",
2429
+ onClick: () => onSelectRepo({
2430
+ owner,
2431
+ repo: name
2432
+ }),
2433
+ className: "flex w-full items-start gap-3 border-b border-gray-800 px-4 py-3 text-left hover:bg-gray-800/60 last:border-b-0 transition-colors",
2434
+ children: [/* @__PURE__ */ jsxs("div", {
2435
+ className: "min-w-0 flex-1",
2436
+ children: [/* @__PURE__ */ jsxs("div", {
2437
+ className: "flex items-center gap-2",
2438
+ children: [/* @__PURE__ */ jsx("span", {
2439
+ className: "text-sm font-medium text-blue-400 truncate",
2440
+ title: repo.full_name,
2441
+ children: repo.full_name
2442
+ }), repo.private && /* @__PURE__ */ jsx(Lock, { className: "h-3 w-3 flex-shrink-0 text-gray-500" })]
2443
+ }), repo.description && /* @__PURE__ */ jsx("p", {
2444
+ className: "mt-0.5 text-xs text-gray-500 truncate",
2445
+ title: repo.description,
2446
+ children: repo.description.length > 80 ? `${repo.description.slice(0, 80)}...` : repo.description
2447
+ })]
2448
+ }), repo.language && /* @__PURE__ */ jsx("span", {
2449
+ className: "flex-shrink-0 rounded-full bg-gray-800 px-2 py-0.5 text-xs text-gray-400",
2450
+ children: repo.language
2451
+ })]
2452
+ }, repo.full_name);
2453
+ })
2454
+ })]
2455
+ });
2456
+ }
2457
+
2458
+ //#endregion
2459
+ //#region src/demos/ServiceDashboardDemo.tsx
2460
+ const ServiceDashboardDemo = observer(({ authBaseURL, initialService, initialCapability, initialPath, onCellChange, onPathChange }) => {
2461
+ const connectionManager = useMemo(() => {
2462
+ const authClient = createConnectAuthClient(authBaseURL);
2463
+ return new ConnectionManagerModel({ authClient });
2464
+ }, [authBaseURL]);
2465
+ const dashboardModel = useMemo(() => {
2466
+ const model = new DashboardModel(connectionManager);
2467
+ if (initialService && initialCapability && connectionManager.isConnected(initialService)) {
2468
+ if (initialPath) model.setBrowserPathSilent(initialPath);
2469
+ model.openCell(initialService, initialCapability);
2470
+ }
2471
+ return model;
2472
+ }, [connectionManager]);
2473
+ dashboardModel.onCellChange = onCellChange;
2474
+ dashboardModel.onPathChange = onPathChange;
2475
+ useEffect(() => {
2476
+ connectionManager.initialize();
2477
+ }, [connectionManager]);
2478
+ const isConnected = initialService ? connectionManager.isConnected(initialService) : false;
2479
+ useEffect(() => {
2480
+ if (!initialService || !initialCapability) {
2481
+ if (dashboardModel.panelOpen) dashboardModel.closePanelSilent();
2482
+ return;
2483
+ }
2484
+ if (!connectionManager.initialized || !isConnected) return;
2485
+ const modelCell = dashboardModel.selectedCell;
2486
+ if (!dashboardModel.panelOpen || modelCell?.serviceId !== initialService || modelCell?.capabilityId !== initialCapability) {
2487
+ if (initialPath) dashboardModel.setBrowserPathSilent(initialPath);
2488
+ dashboardModel.openCell(initialService, initialCapability);
2489
+ }
2490
+ }, [
2491
+ initialService,
2492
+ initialCapability,
2493
+ initialPath,
2494
+ isConnected,
2495
+ connectionManager.initialized,
2496
+ dashboardModel
2497
+ ]);
2498
+ if (connectionManager.configError) return /* @__PURE__ */ jsx("div", {
2499
+ className: "p-4",
2500
+ children: /* @__PURE__ */ jsxs("div", {
2501
+ className: "rounded-lg border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950/30 p-4",
2502
+ children: [/* @__PURE__ */ jsx("p", {
2503
+ className: "text-sm font-medium text-red-800 dark:text-red-300",
2504
+ children: "Dashboard cannot start"
2505
+ }), /* @__PURE__ */ jsx("p", {
2506
+ className: "mt-1 text-sm text-red-600 dark:text-red-400",
2507
+ children: connectionManager.configError
2508
+ })]
2509
+ })
2510
+ });
2511
+ const showNotConnectedBanner = initialService && initialCapability && connectionManager.initialized && !connectionManager.isConnected(initialService) && !dashboardModel.panelOpen;
2512
+ const bannerService = showNotConnectedBanner ? serviceRegistry.get(initialService) : null;
2513
+ return /* @__PURE__ */ jsxs("div", {
2514
+ className: dashboardModel.panelOpen ? "flex-1 flex flex-col min-h-0" : "flex-1 min-h-0 overflow-auto p-4",
2515
+ children: [showNotConnectedBanner && bannerService && /* @__PURE__ */ jsxs("div", {
2516
+ className: "mb-4 rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/30 p-4",
2517
+ children: [/* @__PURE__ */ jsxs("p", {
2518
+ className: "text-sm font-medium text-amber-800 dark:text-amber-300",
2519
+ children: [bannerService.name, " is not connected"]
2520
+ }), /* @__PURE__ */ jsxs("p", {
2521
+ className: "mt-1 text-sm text-amber-600 dark:text-amber-400",
2522
+ children: [
2523
+ "Click the ",
2524
+ /* @__PURE__ */ jsx("strong", { children: "Connect" }),
2525
+ " button next to ",
2526
+ bannerService.name,
2527
+ " in the table below to sign in. The browser will open automatically once connected."
2528
+ ]
2529
+ })]
2530
+ }), /* @__PURE__ */ jsx(ServiceDashboard, {
2531
+ connectionManager,
2532
+ dashboardModel
2533
+ })]
2534
+ });
2535
+ });
2536
+
2537
+ //#endregion
2538
+ export { ActionHistoryPanel, ActionNotificationModel, CapabilityCell, CapabilityPanel, CapabilityPill, ConnectButton, ConnectedMenu, ConnectionManagerModel, ConnectionStatusIndicator, CredentialForm, CredentialFormModel, DashboardModel, FullScreenBrowser, GitHostBrowser, GitHostBrowserModel, GitHubRepoPicker, GitRepoBrowser, GitRepoBrowserModel, ObjectStorageBrowser, ObjectStorageBrowserModel, RepoPicker, RepoPickerModel, ServiceCard, ServiceDashboard, ServiceDashboardDemo, ServiceIcon, ServiceRow, TokenManager, bitbucketService, browserFsService, createConnectAuthClient, dropboxService, getScopeLabel, getScopeLabels, giteaService, githubService, gitlabService, googleService, icloudService, indexeddbService, microsoftService, s3Service, serviceRegistry, showActionToast, showErrorToast, showInfoToast, webdavService };
2539
+ //# sourceMappingURL=index.js.map