agent-relay 2.0.16 → 2.0.18

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 (153) hide show
  1. package/deploy/workspace/entrypoint.sh +35 -19
  2. package/deploy/workspace/git-credential-relay +82 -7
  3. package/dist/dashboard/out/404.html +1 -1
  4. package/dist/dashboard/out/_next/static/chunks/320-402ffc8646b31da1.js +1 -0
  5. package/dist/dashboard/out/_next/static/chunks/83-26d2bde54616ee90.js +1 -0
  6. package/{packages/dashboard/ui-dist/_next/static/chunks/app/app/page-9d6bc8729b429956.js → dist/dashboard/out/_next/static/chunks/app/app/page-366fb7c078d4e9e0.js} +1 -1
  7. package/dist/dashboard/out/_next/static/chunks/app/complete-profile/page-dd64bbdf66b639cd.js +1 -0
  8. package/dist/dashboard/out/_next/static/chunks/app/login/page-435eceb0073be027.js +1 -0
  9. package/dist/dashboard/out/_next/static/chunks/app/{page-487fa38f041815c1.js → page-8119d4246743574e.js} +1 -1
  10. package/dist/dashboard/out/_next/static/chunks/app/signup/page-c7a0a28341365ae0.js +1 -0
  11. package/dist/dashboard/out/_next/static/chunks/{main-5a40a5ae29646e1b.js → main-311c3db74dcfadb7.js} +1 -1
  12. package/dist/dashboard/out/_next/static/css/{605dd4e30c91986f.css → 45361ce86b2847c4.css} +1 -1
  13. package/dist/dashboard/out/app/onboarding.html +1 -1
  14. package/dist/dashboard/out/app/onboarding.txt +1 -1
  15. package/dist/dashboard/out/app.html +1 -1
  16. package/dist/dashboard/out/app.txt +2 -2
  17. package/dist/dashboard/out/cloud/link.html +1 -1
  18. package/dist/dashboard/out/cloud/link.txt +1 -1
  19. package/dist/dashboard/out/complete-profile.html +5 -0
  20. package/dist/dashboard/out/complete-profile.txt +7 -0
  21. package/dist/dashboard/out/connect-repos.html +1 -1
  22. package/dist/dashboard/out/connect-repos.txt +1 -1
  23. package/dist/dashboard/out/history.html +1 -1
  24. package/dist/dashboard/out/history.txt +1 -1
  25. package/dist/dashboard/out/index.html +1 -1
  26. package/dist/dashboard/out/index.txt +2 -2
  27. package/dist/dashboard/out/login.html +2 -2
  28. package/dist/dashboard/out/login.txt +2 -2
  29. package/dist/dashboard/out/metrics.html +1 -1
  30. package/dist/dashboard/out/metrics.txt +1 -1
  31. package/dist/dashboard/out/pricing.html +2 -2
  32. package/dist/dashboard/out/pricing.txt +1 -1
  33. package/dist/dashboard/out/providers/setup/claude.html +1 -1
  34. package/dist/dashboard/out/providers/setup/claude.txt +1 -1
  35. package/dist/dashboard/out/providers/setup/codex.html +1 -1
  36. package/dist/dashboard/out/providers/setup/codex.txt +1 -1
  37. package/dist/dashboard/out/providers/setup/cursor.html +1 -1
  38. package/dist/dashboard/out/providers/setup/cursor.txt +1 -1
  39. package/dist/dashboard/out/providers.html +1 -1
  40. package/dist/dashboard/out/providers.txt +2 -2
  41. package/dist/dashboard/out/signup.html +2 -2
  42. package/dist/dashboard/out/signup.txt +2 -2
  43. package/dist/src/cli/index.js +3 -1
  44. package/package.json +22 -21
  45. package/packages/api-types/package.json +1 -1
  46. package/packages/bridge/package.json +8 -8
  47. package/packages/cloud/dist/api/auth.js +2 -0
  48. package/packages/cloud/dist/api/billing.js +4 -4
  49. package/packages/cloud/dist/api/email-auth.d.ts +11 -0
  50. package/packages/cloud/dist/api/email-auth.js +347 -0
  51. package/packages/cloud/dist/api/nango-auth.js +72 -5
  52. package/packages/cloud/dist/db/drizzle.d.ts +35 -1
  53. package/packages/cloud/dist/db/drizzle.js +136 -0
  54. package/packages/cloud/dist/db/index.d.ts +5 -4
  55. package/packages/cloud/dist/db/index.js +5 -3
  56. package/packages/cloud/dist/db/schema.d.ts +246 -2
  57. package/packages/cloud/dist/db/schema.js +39 -3
  58. package/packages/cloud/dist/provisioner/index.js +5 -1
  59. package/packages/cloud/dist/server.js +134 -24
  60. package/packages/cloud/dist/services/nango.d.ts +18 -0
  61. package/packages/cloud/dist/services/nango.js +32 -0
  62. package/packages/cloud/package.json +6 -6
  63. package/packages/config/package.json +2 -2
  64. package/packages/continuity/package.json +1 -1
  65. package/packages/daemon/package.json +12 -12
  66. package/packages/dashboard/dist/server.js +36 -7
  67. package/packages/dashboard/package.json +13 -13
  68. package/packages/dashboard/ui/app/complete-profile/page.tsx +204 -0
  69. package/packages/dashboard/ui/app/login/page.tsx +182 -38
  70. package/packages/dashboard/ui/app/signup/page.tsx +244 -54
  71. package/packages/dashboard/ui/lib/cloudApi.ts +1 -0
  72. package/packages/dashboard/ui/react-components/App.tsx +1 -1
  73. package/packages/dashboard/ui/react-components/ProviderAuthFlow.tsx +10 -0
  74. package/packages/dashboard/ui/react-components/RepoAccessPanel.tsx +160 -3
  75. package/packages/dashboard/ui-dist/404.html +1 -1
  76. package/packages/dashboard/ui-dist/_next/static/chunks/320-402ffc8646b31da1.js +1 -0
  77. package/packages/dashboard/ui-dist/_next/static/chunks/83-26d2bde54616ee90.js +1 -0
  78. package/{dist/dashboard/out/_next/static/chunks/app/app/page-9d6bc8729b429956.js → packages/dashboard/ui-dist/_next/static/chunks/app/app/page-366fb7c078d4e9e0.js} +1 -1
  79. package/packages/dashboard/ui-dist/_next/static/chunks/app/complete-profile/page-dd64bbdf66b639cd.js +1 -0
  80. package/packages/dashboard/ui-dist/_next/static/chunks/app/login/page-435eceb0073be027.js +1 -0
  81. package/packages/dashboard/ui-dist/_next/static/chunks/app/{page-487fa38f041815c1.js → page-8119d4246743574e.js} +1 -1
  82. package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-c7a0a28341365ae0.js +1 -0
  83. package/packages/dashboard/ui-dist/_next/static/chunks/{main-5a40a5ae29646e1b.js → main-311c3db74dcfadb7.js} +1 -1
  84. package/packages/dashboard/ui-dist/_next/static/css/{605dd4e30c91986f.css → 45361ce86b2847c4.css} +1 -1
  85. package/packages/dashboard/ui-dist/app/onboarding.html +1 -1
  86. package/packages/dashboard/ui-dist/app/onboarding.txt +1 -1
  87. package/packages/dashboard/ui-dist/app.html +1 -1
  88. package/packages/dashboard/ui-dist/app.txt +2 -2
  89. package/packages/dashboard/ui-dist/cloud/link.html +1 -1
  90. package/packages/dashboard/ui-dist/cloud/link.txt +1 -1
  91. package/packages/dashboard/ui-dist/complete-profile.html +5 -0
  92. package/packages/dashboard/ui-dist/complete-profile.txt +7 -0
  93. package/packages/dashboard/ui-dist/connect-repos.html +1 -1
  94. package/packages/dashboard/ui-dist/connect-repos.txt +1 -1
  95. package/packages/dashboard/ui-dist/history.html +1 -1
  96. package/packages/dashboard/ui-dist/history.txt +1 -1
  97. package/packages/dashboard/ui-dist/index.html +1 -1
  98. package/packages/dashboard/ui-dist/index.txt +2 -2
  99. package/packages/dashboard/ui-dist/login.html +2 -2
  100. package/packages/dashboard/ui-dist/login.txt +2 -2
  101. package/packages/dashboard/ui-dist/metrics.html +1 -1
  102. package/packages/dashboard/ui-dist/metrics.txt +1 -1
  103. package/packages/dashboard/ui-dist/pricing.html +2 -2
  104. package/packages/dashboard/ui-dist/pricing.txt +1 -1
  105. package/packages/dashboard/ui-dist/providers/setup/claude.html +1 -1
  106. package/packages/dashboard/ui-dist/providers/setup/claude.txt +1 -1
  107. package/packages/dashboard/ui-dist/providers/setup/codex.html +1 -1
  108. package/packages/dashboard/ui-dist/providers/setup/codex.txt +1 -1
  109. package/packages/dashboard/ui-dist/providers/setup/cursor.html +1 -1
  110. package/packages/dashboard/ui-dist/providers/setup/cursor.txt +1 -1
  111. package/packages/dashboard/ui-dist/providers.html +1 -1
  112. package/packages/dashboard/ui-dist/providers.txt +2 -2
  113. package/packages/dashboard/ui-dist/signup.html +2 -2
  114. package/packages/dashboard/ui-dist/signup.txt +2 -2
  115. package/packages/dashboard-server/dist/server.js +36 -7
  116. package/packages/dashboard-server/package.json +12 -12
  117. package/packages/hooks/package.json +4 -4
  118. package/packages/mcp/package.json +2 -2
  119. package/packages/memory/package.json +2 -2
  120. package/packages/policy/package.json +2 -2
  121. package/packages/protocol/package.json +1 -1
  122. package/packages/resiliency/package.json +1 -1
  123. package/packages/sdk/package.json +2 -2
  124. package/packages/spawner/package.json +1 -1
  125. package/packages/state/package.json +1 -1
  126. package/packages/storage/package.json +2 -2
  127. package/packages/telemetry/package.json +1 -1
  128. package/packages/trajectory/package.json +2 -2
  129. package/packages/user-directory/package.json +2 -2
  130. package/packages/utils/package.json +1 -1
  131. package/packages/wrapper/dist/relay-pty-orchestrator.js +17 -3
  132. package/packages/wrapper/package.json +6 -6
  133. package/relay-snippets/agent-policy-snippet.md +40 -0
  134. package/relay-snippets/agent-relay-protocol.md +101 -0
  135. package/relay-snippets/agent-relay-snippet.md +177 -0
  136. package/SESSION_HANDOFF.md +0 -67
  137. package/dist/dashboard/out/_next/static/chunks/320-23e5ffe6aa7eb934.js +0 -1
  138. package/dist/dashboard/out/_next/static/chunks/83-4f08122d4e7e79a6.js +0 -1
  139. package/dist/dashboard/out/_next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js +0 -1
  140. package/dist/dashboard/out/_next/static/chunks/app/signup/page-1ede2205b58649ca.js +0 -1
  141. package/packages/dashboard/ui-dist/_next/static/chunks/320-23e5ffe6aa7eb934.js +0 -1
  142. package/packages/dashboard/ui-dist/_next/static/chunks/83-4f08122d4e7e79a6.js +0 -1
  143. package/packages/dashboard/ui-dist/_next/static/chunks/app/login/page-a0ca6f7ca6a100b8.js +0 -1
  144. package/packages/dashboard/ui-dist/_next/static/chunks/app/signup/page-1ede2205b58649ca.js +0 -1
  145. package/test-push.txt +0 -1
  146. /package/dist/dashboard/out/_next/static/{itBGQ1M8yMA_hC42DKCqv → JIjqkuDKNeoSg7KaMMuhx}/_buildManifest.js +0 -0
  147. /package/dist/dashboard/out/_next/static/{itBGQ1M8yMA_hC42DKCqv → JIjqkuDKNeoSg7KaMMuhx}/_ssgManifest.js +0 -0
  148. /package/packages/dashboard/ui-dist/_next/static/{ML6Zby1B5OtZvl0Pa1zSZ → JIjqkuDKNeoSg7KaMMuhx}/_buildManifest.js +0 -0
  149. /package/packages/dashboard/ui-dist/_next/static/{ML6Zby1B5OtZvl0Pa1zSZ → JIjqkuDKNeoSg7KaMMuhx}/_ssgManifest.js +0 -0
  150. /package/packages/dashboard/ui-dist/_next/static/{Ni5Di0TB0PDcrvEYBFRKd → nmkOi7bqeDmLMoWBih8lz}/_buildManifest.js +0 -0
  151. /package/packages/dashboard/ui-dist/_next/static/{Ni5Di0TB0PDcrvEYBFRKd → nmkOi7bqeDmLMoWBih8lz}/_ssgManifest.js +0 -0
  152. /package/packages/dashboard/ui-dist/_next/static/{itBGQ1M8yMA_hC42DKCqv → wk_gKRNSPpWE-ZhGL6UMl}/_buildManifest.js +0 -0
  153. /package/packages/dashboard/ui-dist/_next/static/{itBGQ1M8yMA_hC42DKCqv → wk_gKRNSPpWE-ZhGL6UMl}/_ssgManifest.js +0 -0
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Signup Page - GitHub OAuth via Nango
2
+ * Signup Page - GitHub OAuth via Nango or Email/Password
3
3
  *
4
4
  * Key: Initialize Nango on page load, not on click.
5
5
  * This avoids popup blockers by ensuring openConnectUI is synchronous.
@@ -11,7 +11,10 @@ import React, { useState, useEffect, useRef } from 'react';
11
11
  import Nango from '@nangohq/frontend';
12
12
  import { LogoIcon } from '../../react-components/Logo';
13
13
 
14
+ type AuthMethod = 'github' | 'email';
15
+
14
16
  export default function SignupPage() {
17
+ const [authMethod, setAuthMethod] = useState<AuthMethod>('github');
15
18
  const [isReady, setIsReady] = useState(false);
16
19
  const [isAuthenticating, setIsAuthenticating] = useState(false);
17
20
  const [authStatus, setAuthStatus] = useState<string>('');
@@ -19,6 +22,12 @@ export default function SignupPage() {
19
22
  const [redirectTarget, setRedirectTarget] = useState<string>('/app');
20
23
  const [showSuccess, setShowSuccess] = useState(false);
21
24
 
25
+ // Email signup form state
26
+ const [email, setEmail] = useState('');
27
+ const [password, setPassword] = useState('');
28
+ const [confirmPassword, setConfirmPassword] = useState('');
29
+ const [displayName, setDisplayName] = useState('');
30
+
22
31
  // Store Nango instance - initialized on mount
23
32
  const nangoRef = useRef<InstanceType<typeof Nango> | null>(null);
24
33
 
@@ -49,7 +58,8 @@ export default function SignupPage() {
49
58
  if (!mounted) return;
50
59
 
51
60
  if (!response.ok || !data.sessionToken) {
52
- setError('Failed to initialize. Please refresh the page.');
61
+ // Don't set error - email signup doesn't need Nango
62
+ setIsReady(true);
53
63
  return;
54
64
  }
55
65
 
@@ -59,7 +69,8 @@ export default function SignupPage() {
59
69
  } catch (err) {
60
70
  if (mounted) {
61
71
  console.error('Init error:', err);
62
- setError('Failed to initialize. Please refresh the page.');
72
+ // Still allow email signup even if Nango fails
73
+ setIsReady(true);
63
74
  }
64
75
  }
65
76
  };
@@ -98,7 +109,7 @@ export default function SignupPage() {
98
109
  }
99
110
  };
100
111
 
101
- const checkAuthStatus = async (connectionId: string): Promise<{ ready: boolean }> => {
112
+ const checkAuthStatus = async (connectionId: string): Promise<{ ready: boolean; needsEmail?: boolean }> => {
102
113
  const response = await fetch(`/api/auth/nango/login-status/${connectionId}`, {
103
114
  credentials: 'include',
104
115
  });
@@ -126,6 +137,11 @@ export default function SignupPage() {
126
137
  try {
127
138
  const result = await checkAuthStatus(connectionId);
128
139
  if (result && result.ready) {
140
+ // If user needs to provide email, redirect to complete-profile
141
+ if (result.needsEmail) {
142
+ window.location.href = '/complete-profile';
143
+ return;
144
+ }
129
145
  await handlePostAuthRedirect();
130
146
  return;
131
147
  }
@@ -150,7 +166,7 @@ export default function SignupPage() {
150
166
  // Use nango.auth() instead of openConnectUI to avoid popup blocker issues
151
167
  const handleGitHubAuth = async () => {
152
168
  if (!nangoRef.current) {
153
- setError('Not ready. Please refresh the page.');
169
+ setError('GitHub signup not available. Please use email signup or refresh the page.');
154
170
  return;
155
171
  }
156
172
 
@@ -191,6 +207,70 @@ export default function SignupPage() {
191
207
  }
192
208
  };
193
209
 
210
+ // Handle email signup
211
+ const handleEmailSignup = async (e: React.FormEvent) => {
212
+ e.preventDefault();
213
+ setError('');
214
+
215
+ // Validate passwords match
216
+ if (password !== confirmPassword) {
217
+ setError('Passwords do not match');
218
+ return;
219
+ }
220
+
221
+ // Validate password length
222
+ if (password.length < 8) {
223
+ setError('Password must be at least 8 characters long');
224
+ return;
225
+ }
226
+
227
+ setIsAuthenticating(true);
228
+ setAuthStatus('Creating your account...');
229
+
230
+ try {
231
+ // Get CSRF token first
232
+ const csrfResponse = await fetch('/api/auth/session', { credentials: 'include' });
233
+ const csrfToken = csrfResponse.headers.get('x-csrf-token');
234
+
235
+ const response = await fetch('/api/auth/email/signup', {
236
+ method: 'POST',
237
+ headers: {
238
+ 'Content-Type': 'application/json',
239
+ ...(csrfToken && { 'x-csrf-token': csrfToken }),
240
+ },
241
+ credentials: 'include',
242
+ body: JSON.stringify({
243
+ email,
244
+ password,
245
+ displayName: displayName || undefined,
246
+ }),
247
+ });
248
+
249
+ const data = await response.json();
250
+
251
+ if (!response.ok) {
252
+ setError(data.error || 'Signup failed');
253
+ setIsAuthenticating(false);
254
+ setAuthStatus('');
255
+ return;
256
+ }
257
+
258
+ // Success - redirect to app
259
+ setAuthStatus('Account created! Redirecting...');
260
+ setShowSuccess(true);
261
+ setRedirectTarget('/app');
262
+
263
+ setTimeout(() => {
264
+ window.location.href = '/app';
265
+ }, 1500);
266
+ } catch (err) {
267
+ console.error('Email signup error:', err);
268
+ setError('Failed to connect. Please try again.');
269
+ setIsAuthenticating(false);
270
+ setAuthStatus('');
271
+ }
272
+ };
273
+
194
274
  const isLoading = !isReady || isAuthenticating;
195
275
 
196
276
  return (
@@ -249,7 +329,7 @@ export default function SignupPage() {
249
329
  </svg>
250
330
  </div>
251
331
  <h2 className="text-xl font-semibold text-white mb-2">Creating Account</h2>
252
- <p className="text-text-muted">{authStatus || 'Connecting to GitHub...'}</p>
332
+ <p className="text-text-muted">{authStatus || 'Setting things up...'}</p>
253
333
  </div>
254
334
  ) : (
255
335
  <div>
@@ -259,57 +339,167 @@ export default function SignupPage() {
259
339
  </div>
260
340
  )}
261
341
 
262
- {/* Features list */}
263
- <div className="mb-6 space-y-3">
264
- <div className="flex items-center gap-3 text-sm text-text-secondary">
265
- <div className="w-8 h-8 rounded-lg bg-accent-cyan/10 flex items-center justify-center flex-shrink-0">
266
- <svg className="w-4 h-4 text-accent-cyan" fill="none" viewBox="0 0 24 24" stroke="currentColor">
267
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
268
- </svg>
342
+ {/* Auth method tabs */}
343
+ <div className="flex mb-6 bg-bg-secondary/50 rounded-lg p-1">
344
+ <button
345
+ type="button"
346
+ onClick={() => setAuthMethod('github')}
347
+ className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
348
+ authMethod === 'github'
349
+ ? 'bg-bg-primary text-white shadow-sm'
350
+ : 'text-text-muted hover:text-white'
351
+ }`}
352
+ >
353
+ GitHub
354
+ </button>
355
+ <button
356
+ type="button"
357
+ onClick={() => setAuthMethod('email')}
358
+ className={`flex-1 py-2 px-4 rounded-md text-sm font-medium transition-colors ${
359
+ authMethod === 'email'
360
+ ? 'bg-bg-primary text-white shadow-sm'
361
+ : 'text-text-muted hover:text-white'
362
+ }`}
363
+ >
364
+ Email
365
+ </button>
366
+ </div>
367
+
368
+ {authMethod === 'github' ? (
369
+ <>
370
+ {/* Features list */}
371
+ <div className="mb-6 space-y-3">
372
+ <div className="flex items-center gap-3 text-sm text-text-secondary">
373
+ <div className="w-8 h-8 rounded-lg bg-accent-cyan/10 flex items-center justify-center flex-shrink-0">
374
+ <svg className="w-4 h-4 text-accent-cyan" fill="none" viewBox="0 0 24 24" stroke="currentColor">
375
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
376
+ </svg>
377
+ </div>
378
+ <span>Deploy AI agents in seconds</span>
379
+ </div>
380
+ <div className="flex items-center gap-3 text-sm text-text-secondary">
381
+ <div className="w-8 h-8 rounded-lg bg-[#00ffc8]/10 flex items-center justify-center flex-shrink-0">
382
+ <svg className="w-4 h-4 text-[#00ffc8]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
383
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
384
+ </svg>
385
+ </div>
386
+ <span>Real-time agent collaboration</span>
387
+ </div>
388
+ <div className="flex items-center gap-3 text-sm text-text-secondary">
389
+ <div className="w-8 h-8 rounded-lg bg-[#0891b2]/10 flex items-center justify-center flex-shrink-0">
390
+ <svg className="w-4 h-4 text-[#0891b2]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
391
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
392
+ </svg>
393
+ </div>
394
+ <span>Secure credential management</span>
395
+ </div>
269
396
  </div>
270
- <span>Deploy AI agents in seconds</span>
271
- </div>
272
- <div className="flex items-center gap-3 text-sm text-text-secondary">
273
- <div className="w-8 h-8 rounded-lg bg-[#00ffc8]/10 flex items-center justify-center flex-shrink-0">
274
- <svg className="w-4 h-4 text-[#00ffc8]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
275
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
276
- </svg>
397
+
398
+ <button
399
+ type="button"
400
+ onClick={handleGitHubAuth}
401
+ disabled={isLoading}
402
+ className="w-full py-4 px-6 bg-[#24292e] hover:bg-[#2f363d] border border-[#444d56] rounded-xl text-white font-medium flex items-center justify-center gap-3 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
403
+ >
404
+ {!isReady ? (
405
+ <>
406
+ <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
407
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
408
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
409
+ </svg>
410
+ <span>Loading...</span>
411
+ </>
412
+ ) : (
413
+ <>
414
+ <svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
415
+ <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
416
+ </svg>
417
+ <span>Sign up with GitHub</span>
418
+ </>
419
+ )}
420
+ </button>
421
+ </>
422
+ ) : (
423
+ <form onSubmit={handleEmailSignup} className="space-y-4">
424
+ <div>
425
+ <label htmlFor="displayName" className="block text-sm font-medium text-text-secondary mb-2">
426
+ Name <span className="text-text-muted">(optional)</span>
427
+ </label>
428
+ <input
429
+ type="text"
430
+ id="displayName"
431
+ value={displayName}
432
+ onChange={(e) => setDisplayName(e.target.value)}
433
+ disabled={isAuthenticating}
434
+ className="w-full px-4 py-3 bg-bg-secondary border border-border-subtle rounded-xl text-white placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 focus:border-accent-cyan disabled:opacity-50"
435
+ placeholder="Your name"
436
+ />
277
437
  </div>
278
- <span>Real-time agent collaboration</span>
279
- </div>
280
- <div className="flex items-center gap-3 text-sm text-text-secondary">
281
- <div className="w-8 h-8 rounded-lg bg-[#0891b2]/10 flex items-center justify-center flex-shrink-0">
282
- <svg className="w-4 h-4 text-[#0891b2]" fill="none" viewBox="0 0 24 24" stroke="currentColor">
283
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
284
- </svg>
438
+ <div>
439
+ <label htmlFor="email" className="block text-sm font-medium text-text-secondary mb-2">
440
+ Email
441
+ </label>
442
+ <input
443
+ type="email"
444
+ id="email"
445
+ value={email}
446
+ onChange={(e) => setEmail(e.target.value)}
447
+ required
448
+ disabled={isAuthenticating}
449
+ className="w-full px-4 py-3 bg-bg-secondary border border-border-subtle rounded-xl text-white placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 focus:border-accent-cyan disabled:opacity-50"
450
+ placeholder="you@example.com"
451
+ />
285
452
  </div>
286
- <span>Secure credential management</span>
287
- </div>
288
- </div>
289
-
290
- <button
291
- type="button"
292
- onClick={handleGitHubAuth}
293
- disabled={isLoading}
294
- className="w-full py-4 px-6 bg-[#24292e] hover:bg-[#2f363d] border border-[#444d56] rounded-xl text-white font-medium flex items-center justify-center gap-3 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
295
- >
296
- {!isReady ? (
297
- <>
298
- <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
299
- <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
300
- <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
301
- </svg>
302
- <span>Loading...</span>
303
- </>
304
- ) : (
305
- <>
306
- <svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
307
- <path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
308
- </svg>
309
- <span>Sign up with GitHub</span>
310
- </>
311
- )}
312
- </button>
453
+ <div>
454
+ <label htmlFor="password" className="block text-sm font-medium text-text-secondary mb-2">
455
+ Password
456
+ </label>
457
+ <input
458
+ type="password"
459
+ id="password"
460
+ value={password}
461
+ onChange={(e) => setPassword(e.target.value)}
462
+ required
463
+ minLength={8}
464
+ disabled={isAuthenticating}
465
+ className="w-full px-4 py-3 bg-bg-secondary border border-border-subtle rounded-xl text-white placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 focus:border-accent-cyan disabled:opacity-50"
466
+ placeholder="At least 8 characters"
467
+ />
468
+ </div>
469
+ <div>
470
+ <label htmlFor="confirmPassword" className="block text-sm font-medium text-text-secondary mb-2">
471
+ Confirm Password
472
+ </label>
473
+ <input
474
+ type="password"
475
+ id="confirmPassword"
476
+ value={confirmPassword}
477
+ onChange={(e) => setConfirmPassword(e.target.value)}
478
+ required
479
+ disabled={isAuthenticating}
480
+ className="w-full px-4 py-3 bg-bg-secondary border border-border-subtle rounded-xl text-white placeholder-text-muted focus:outline-none focus:ring-2 focus:ring-accent-cyan/50 focus:border-accent-cyan disabled:opacity-50"
481
+ placeholder="Confirm your password"
482
+ />
483
+ </div>
484
+ <button
485
+ type="submit"
486
+ disabled={isLoading || !email || !password || !confirmPassword}
487
+ className="w-full py-4 px-6 bg-accent-cyan hover:bg-accent-cyan/90 rounded-xl text-black font-medium flex items-center justify-center gap-3 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
488
+ >
489
+ {isAuthenticating ? (
490
+ <>
491
+ <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
492
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
493
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
494
+ </svg>
495
+ <span>{authStatus || 'Creating account...'}</span>
496
+ </>
497
+ ) : (
498
+ <span>Create Account</span>
499
+ )}
500
+ </button>
501
+ </form>
502
+ )}
313
503
 
314
504
  <p className="mt-6 text-center text-text-muted text-sm">
315
505
  By signing up, you agree to our{' '}
@@ -42,6 +42,7 @@ export interface CloudUser {
42
42
  }>;
43
43
  pendingInvites: number;
44
44
  onboardingCompleted: boolean;
45
+ displayName?: string;
45
46
  }
46
47
 
47
48
  export type SessionExpiredCallback = (error: SessionError) => void;
@@ -221,7 +221,7 @@ export function App({ wsUrl, orchestratorUrl }: AppProps) {
221
221
  // Derive current user from cloud session (falls back to undefined in non-cloud mode)
222
222
  const currentUser: CurrentUser | undefined = cloudSession?.user
223
223
  ? {
224
- displayName: cloudSession.user.githubUsername,
224
+ displayName: cloudSession.user.githubUsername || cloudSession.user.displayName || '',
225
225
  avatarUrl: cloudSession.user.avatarUrl,
226
226
  }
227
227
  : undefined;
@@ -65,6 +65,7 @@ export function ProviderAuthFlow({
65
65
  const popupOpenedRef = useRef(false);
66
66
  const pollingRef = useRef(false);
67
67
  const cliPollingRef = useRef(false);
68
+ const completingRef = useRef(false); // Prevent double-calling handleComplete
68
69
 
69
70
  const backendProviderId = PROVIDER_ID_MAP[provider.id] || provider.id;
70
71
 
@@ -320,6 +321,14 @@ export function ProviderAuthFlow({
320
321
  const targetSessionId = sid || sessionId;
321
322
  if (!targetSessionId) return;
322
323
 
324
+ // Prevent double-calling (both CLI polling and status polling may detect success)
325
+ if (completingRef.current) return;
326
+ completingRef.current = true;
327
+
328
+ // Stop both polling loops
329
+ pollingRef.current = false;
330
+ cliPollingRef.current = false;
331
+
323
332
  setStatus('submitting');
324
333
  setErrorMessage(null);
325
334
 
@@ -343,6 +352,7 @@ export function ProviderAuthFlow({
343
352
  // Brief delay to show success message before parent unmounts component
344
353
  setTimeout(() => onSuccess(), 1500);
345
354
  } catch (err) {
355
+ completingRef.current = false; // Allow retry on error
346
356
  const msg = err instanceof Error ? err.message : 'Failed to complete authentication';
347
357
  setErrorMessage(msg);
348
358
  setStatus('error');
@@ -9,7 +9,8 @@
9
9
  * - POST /api/workspaces/quick - Create workspace for a repo
10
10
  */
11
11
 
12
- import React, { useState, useEffect, useCallback } from 'react';
12
+ import React, { useState, useEffect, useCallback, useRef } from 'react';
13
+ import Nango from '@nangohq/frontend';
13
14
 
14
15
  interface AccessibleRepo {
15
16
  id: number;
@@ -69,10 +70,17 @@ export function RepoAccessPanel({
69
70
  const [repos, setRepos] = useState<AccessibleRepo[]>([]);
70
71
  const [loadingState, setLoadingState] = useState<LoadingState>('idle');
71
72
  const [error, setError] = useState<string | null>(null);
73
+ const [isGitHubNotConnected, setIsGitHubNotConnected] = useState(false);
72
74
  const [creatingWorkspace, setCreatingWorkspace] = useState<string | null>(null);
73
75
  const [searchQuery, setSearchQuery] = useState('');
74
76
  const [filterType, setFilterType] = useState<'all' | 'with-workspace' | 'without-workspace'>('all');
75
77
 
78
+ // GitHub OAuth state
79
+ const nangoRef = useRef<InstanceType<typeof Nango> | null>(null);
80
+ const [isNangoReady, setIsNangoReady] = useState(false);
81
+ const [isConnecting, setIsConnecting] = useState(false);
82
+ const [connectError, setConnectError] = useState<string | null>(null);
83
+
76
84
  // Create a map of repo full names to workspace IDs for quick lookup
77
85
  const repoToWorkspace = new Map<string, Workspace>();
78
86
  workspaces.forEach(ws => {
@@ -85,6 +93,7 @@ export function RepoAccessPanel({
85
93
  const fetchRepos = useCallback(async () => {
86
94
  setLoadingState('loading');
87
95
  setError(null);
96
+ setIsGitHubNotConnected(false);
88
97
 
89
98
  try {
90
99
  const response = await fetch('/api/repos/accessible?perPage=100', {
@@ -94,7 +103,8 @@ export function RepoAccessPanel({
94
103
  if (!response.ok) {
95
104
  const data = await response.json();
96
105
  if (data.code === 'NANGO_NOT_CONNECTED') {
97
- throw new Error('GitHub not connected. Please reconnect your GitHub account.');
106
+ setIsGitHubNotConnected(true);
107
+ throw new Error('GitHub not connected. Connect your GitHub account to see your repositories.');
98
108
  }
99
109
  throw new Error(data.error || 'Failed to fetch repositories');
100
110
  }
@@ -113,6 +123,98 @@ export function RepoAccessPanel({
113
123
  fetchRepos();
114
124
  }, [fetchRepos]);
115
125
 
126
+ // Initialize Nango when GitHub is not connected
127
+ useEffect(() => {
128
+ if (!isGitHubNotConnected) return;
129
+
130
+ let mounted = true;
131
+
132
+ const initNango = async () => {
133
+ try {
134
+ // Get Nango session token for GitHub login
135
+ const response = await fetch('/api/auth/nango/login-session', {
136
+ credentials: 'include',
137
+ });
138
+ const data = await response.json();
139
+
140
+ if (!mounted) return;
141
+
142
+ if (response.ok && data.sessionToken) {
143
+ nangoRef.current = new Nango({ connectSessionToken: data.sessionToken });
144
+ setIsNangoReady(true);
145
+ }
146
+ } catch (err) {
147
+ console.error('Failed to initialize Nango:', err);
148
+ }
149
+ };
150
+
151
+ initNango();
152
+ return () => { mounted = false; };
153
+ }, [isGitHubNotConnected]);
154
+
155
+ // Handle GitHub OAuth connection
156
+ const handleConnectGitHub = async () => {
157
+ if (!nangoRef.current) {
158
+ setConnectError('GitHub connection not available. Please refresh the page.');
159
+ return;
160
+ }
161
+
162
+ setIsConnecting(true);
163
+ setConnectError(null);
164
+
165
+ try {
166
+ const result = await nangoRef.current.auth('github');
167
+ if (result && 'connectionId' in result) {
168
+ // Poll for auth completion
169
+ const pollForAuth = async (attempts = 0): Promise<void> => {
170
+ if (attempts > 30) {
171
+ throw new Error('Authentication timed out. Please try again.');
172
+ }
173
+
174
+ const statusRes = await fetch(`/api/auth/nango/login-status/${result.connectionId}`, {
175
+ credentials: 'include',
176
+ });
177
+ const statusData = await statusRes.json();
178
+
179
+ if (statusData.ready) {
180
+ // Auth complete, refresh repos
181
+ setIsConnecting(false);
182
+ setIsGitHubNotConnected(false);
183
+ fetchRepos();
184
+ return;
185
+ }
186
+
187
+ await new Promise(resolve => setTimeout(resolve, 1000));
188
+ return pollForAuth(attempts + 1);
189
+ };
190
+
191
+ await pollForAuth();
192
+ } else {
193
+ throw new Error('No connection ID returned');
194
+ }
195
+ } catch (err: unknown) {
196
+ const error = err as Error & { type?: string };
197
+ console.error('GitHub auth error:', error);
198
+
199
+ // Don't show error for user-cancelled auth
200
+ if (error.type === 'user_cancelled' || error.message?.includes('closed')) {
201
+ setIsConnecting(false);
202
+ // Re-initialize Nango for next attempt
203
+ fetch('/api/auth/nango/login-session', { credentials: 'include' })
204
+ .then(res => res.json())
205
+ .then(data => {
206
+ if (data.sessionToken) {
207
+ nangoRef.current = new Nango({ connectSessionToken: data.sessionToken });
208
+ }
209
+ });
210
+ return;
211
+ }
212
+
213
+ setConnectError(error.message || 'Failed to connect GitHub');
214
+ setIsConnecting(false);
215
+ }
216
+ };
217
+
116
218
  // Create workspace for a repo
117
219
  const handleCreateWorkspace = useCallback(async (repoFullName: string) => {
118
220
  setCreatingWorkspace(repoFullName);
@@ -176,8 +278,55 @@ export function RepoAccessPanel({
176
278
  );
177
279
  }
178
280
 
179
- // Error state
281
+ // Error state - special handling for GitHub not connected
180
282
  if (loadingState === 'error') {
283
+ if (isGitHubNotConnected) {
284
+ return (
285
+ <div className={`p-6 ${className}`}>
286
+ <div className="bg-bg-tertiary border border-border-subtle rounded-xl p-8 text-center">
287
+ <div className="w-16 h-16 mx-auto mb-4 bg-bg-hover rounded-full flex items-center justify-center">
288
+ <GitHubIcon className="w-8 h-8 text-text-muted" />
289
+ </div>
290
+ <h3 className="text-lg font-semibold text-text-primary mb-2">Connect GitHub</h3>
291
+ <p className="text-text-muted mb-6 max-w-md mx-auto">
292
+ Connect your GitHub account to see your repositories and enable agent access to your code.
293
+ </p>
294
+ {connectError && (
295
+ <p className="text-error text-sm mb-4">{connectError}</p>
296
+ )}
297
+ <button
298
+ onClick={handleConnectGitHub}
299
+ disabled={!isNangoReady || isConnecting}
300
+ className="px-6 py-3 bg-gradient-to-r from-accent-cyan to-[#00b8d9] text-bg-deep font-medium rounded-lg hover:shadow-glow-cyan transition-all disabled:opacity-50 disabled:cursor-not-allowed"
301
+ >
302
+ {isConnecting ? (
303
+ <span className="flex items-center gap-2">
304
+ <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
305
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
306
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
307
+ </svg>
308
+ Connecting...
309
+ </span>
310
+ ) : !isNangoReady ? (
311
+ <span className="flex items-center gap-2">
312
+ <svg className="w-5 h-5 animate-spin" fill="none" viewBox="0 0 24 24">
313
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
314
+ <path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
315
+ </svg>
316
+ Loading...
317
+ </span>
318
+ ) : (
319
+ <span className="flex items-center gap-2">
320
+ <GitHubIcon className="w-5 h-5" />
321
+ Connect GitHub Account
322
+ </span>
323
+ )}
324
+ </button>
325
+ </div>
326
+ </div>
327
+ );
328
+ }
329
+
181
330
  return (
182
331
  <div className={`p-6 ${className}`}>
183
332
  <div className="bg-error/10 border border-error/20 rounded-xl p-4 text-center">
@@ -390,3 +539,11 @@ function RepoIcon({ className = '' }: { className?: string }) {
390
539
  </svg>
391
540
  );
392
541
  }
542
+
543
+ function GitHubIcon({ className = '' }: { className?: string }) {
544
+ return (
545
+ <svg className={className} viewBox="0 0 24 24" fill="currentColor">
546
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
547
+ </svg>
548
+ );
549
+ }