@agent-relay/dashboard 2.0.82 → 2.0.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/404.html +1 -1
- package/out/_next/static/chunks/1028-da5d75e35d1420f1.js +1 -0
- package/out/_next/static/chunks/1528-78b17000a7e10bc6.js +2 -0
- package/out/_next/static/chunks/1695-4a5d33ba715e09b4.js +1 -0
- package/out/_next/static/chunks/1705-36c2180d00a4a569.js +1 -0
- package/out/_next/static/chunks/1dd3208c-e1f87c7b3dc1a820.js +1 -0
- package/out/_next/static/chunks/3663-47290254b8f6f5dd.js +1 -0
- package/out/_next/static/chunks/3677-4b225baf4801d9b9.js +73 -0
- package/out/_next/static/chunks/5118-7e8ada2df38eef07.js +1 -0
- package/out/_next/static/chunks/5888-15cbe97c90ed5fae.js +1 -0
- package/out/_next/static/chunks/6773-a45343a98df3abb5.js +1 -0
- package/out/_next/static/chunks/6940-b824612b605e79b3.js +9 -0
- package/out/_next/static/chunks/7894-f4a15249082a680d.js +1 -0
- package/out/_next/static/chunks/9175-b3617c1e5cbfed0e.js +1 -0
- package/out/_next/static/chunks/9372-1a804b8d08c7a236.js +1 -0
- package/out/_next/static/chunks/{ab6c8a12-0a58072fbb505134.js → ab6c8a12-91438a812d94ecf0.js} +1 -1
- package/out/_next/static/chunks/app/_not-found/page-8e8842f82d204726.js +1 -0
- package/out/_next/static/chunks/app/about/page-b78577a7da8fa459.js +1 -0
- package/out/_next/static/chunks/app/app/[[...slug]]/page-3dffd65b6344f53e.js +1 -0
- package/out/_next/static/chunks/app/app/onboarding/page-b89be9aa6264a5e1.js +1 -0
- package/out/_next/static/chunks/app/blog/go-to-bed-wake-up-to-a-finished-product/page-fbd00893ef69e499.js +1 -0
- package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/page-de2ea13649d0b6d3.js +1 -0
- package/out/_next/static/chunks/app/blog/page-a08e263c57a156fa.js +1 -0
- package/out/_next/static/chunks/app/careers/page-02228e1d6969b232.js +1 -0
- package/out/_next/static/chunks/app/changelog/page-1b5c1d79efc6e53a.js +1 -0
- package/out/_next/static/chunks/app/cloud/link/page-99654edffffb3af2.js +1 -0
- package/out/_next/static/chunks/app/complete-profile/page-59d146e5ddeafc5c.js +1 -0
- package/out/_next/static/chunks/app/connect-repos/page-995e16a976a6632c.js +1 -0
- package/out/_next/static/chunks/app/contact/page-273396a5ad57bcee.js +1 -0
- package/out/_next/static/chunks/app/dev/cli-tools/page-a71b80dcb2d5fc8d.js +1 -0
- package/out/_next/static/chunks/app/dev/log-viewer/page-46a6151ae1be0796.js +1 -0
- package/out/_next/static/chunks/app/docs/page-7c7cb603b24b7c40.js +1 -0
- package/out/_next/static/chunks/app/history/page-0c5cab1dab4e8886.js +1 -0
- package/out/_next/static/chunks/app/layout-96d72ba8ef8a43a0.js +1 -0
- package/out/_next/static/chunks/app/login/page-0ccbab34213df842.js +1 -0
- package/out/_next/static/chunks/app/metrics/page-8616272aeab9c8b0.js +1 -0
- package/out/_next/static/chunks/app/page-09ce10603ad9a251.js +1 -0
- package/out/_next/static/chunks/app/pricing/page-91c975079120c941.js +1 -0
- package/out/_next/static/chunks/app/privacy/{page-c21d51ac2dee3a88.js → page-a49ab271cc686644.js} +1 -1
- package/out/_next/static/chunks/app/providers/{page-59114505f4353512.js → page-d775d6eb5bc29e96.js} +1 -1
- package/out/_next/static/chunks/app/providers/setup/[provider]/page-ec4ef3cd80de807e.js +1 -0
- package/out/_next/static/chunks/app/security/page-d9da9bd9191e8f95.js +1 -0
- package/out/_next/static/chunks/app/signup/page-930eca0bf5fd299d.js +1 -0
- package/out/_next/static/chunks/app/terms/page-3e4827620b98613c.js +1 -0
- package/out/_next/static/chunks/framework-648e1ae7da590300.js +1 -0
- package/out/_next/static/chunks/{main-acb1b24265295d6a.js → main-2b1990080c292d92.js} +1 -1
- package/out/_next/static/chunks/main-app-9f6b7ff9e754a8f5.js +1 -0
- package/out/_next/static/chunks/pages/_app-a077b72e02273ab1.js +1 -0
- package/out/_next/static/chunks/pages/_error-84001666436a04e4.js +1 -0
- package/out/_next/static/chunks/{webpack-dd93b81e2659669c.js → webpack-7586035f1585f2db.js} +1 -1
- package/out/_next/static/css/eb9fc69d1e3d2bed.css +1 -0
- package/out/_next/static/{IxfA6RZu4trcsEMYlkQra → g3G0LMdB7lxcrU5mdM54m}/_buildManifest.js +1 -1
- package/out/about.html +2 -2
- package/out/about.txt +2 -2
- package/out/app/onboarding.html +1 -1
- package/out/app/onboarding.txt +2 -2
- package/out/app.html +1 -1
- package/out/app.txt +2 -2
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.html +3 -3
- package/out/blog/go-to-bed-wake-up-to-a-finished-product.txt +1 -1
- package/out/blog/let-them-cook-multi-agent-orchestration.html +2 -2
- package/out/blog/let-them-cook-multi-agent-orchestration.txt +2 -2
- package/out/blog.html +2 -2
- package/out/blog.txt +1 -1
- package/out/careers.html +2 -2
- package/out/careers.txt +2 -2
- package/out/changelog.html +2 -2
- package/out/changelog.txt +2 -2
- package/out/cloud/link.html +1 -1
- package/out/cloud/link.txt +2 -2
- package/out/complete-profile.html +2 -2
- package/out/complete-profile.txt +2 -2
- package/out/connect-repos.html +1 -1
- package/out/connect-repos.txt +2 -2
- package/out/contact.html +2 -2
- package/out/contact.txt +2 -2
- package/out/dev/cli-tools.html +1 -0
- package/out/dev/cli-tools.txt +7 -0
- package/out/dev/log-viewer.html +23 -0
- package/out/dev/log-viewer.txt +7 -0
- package/out/docs.html +2 -2
- package/out/docs.txt +2 -2
- package/out/history.html +1 -1
- package/out/history.txt +2 -2
- package/out/index.html +1 -1
- package/out/index.txt +2 -2
- package/out/login.html +2 -2
- package/out/login.txt +2 -2
- package/out/metrics.html +1 -1
- package/out/metrics.txt +2 -2
- package/out/pricing.html +2 -2
- package/out/pricing.txt +2 -2
- package/out/privacy.html +2 -2
- package/out/privacy.txt +2 -2
- package/out/providers/setup/claude.html +1 -1
- package/out/providers/setup/claude.txt +2 -2
- package/out/providers/setup/codex.html +1 -1
- package/out/providers/setup/codex.txt +2 -2
- package/out/providers/setup/cursor.html +1 -1
- package/out/providers/setup/cursor.txt +2 -2
- package/out/providers.html +1 -1
- package/out/providers.txt +2 -2
- package/out/security.html +2 -2
- package/out/security.txt +2 -2
- package/out/signup.html +2 -2
- package/out/signup.txt +2 -2
- package/out/terms.html +2 -2
- package/out/terms.txt +2 -2
- package/package.json +5 -1
- package/src/adapters/DashboardConfigProvider.tsx +56 -0
- package/src/adapters/cloudFetchAdapter.ts +278 -0
- package/src/adapters/index.ts +3 -0
- package/src/adapters/types.ts +508 -0
- package/src/app/app/[[...slug]]/DashboardPageClient.tsx +67 -18
- package/src/app/app/onboarding/page.tsx +870 -170
- package/src/app/cloud/link/page.tsx +14 -6
- package/src/app/connect-repos/page.tsx +9 -3
- package/src/app/dev/cli-tools/page.tsx +130 -0
- package/src/app/dev/log-viewer/MockLogViewer.tsx +132 -0
- package/src/app/dev/log-viewer/fixtures.ts +110 -0
- package/src/app/dev/log-viewer/page.tsx +288 -0
- package/src/app/history/page.tsx +28 -12
- package/src/app/page.tsx +1 -1
- package/src/app/providers/setup/[provider]/ProviderSetupClient.tsx +209 -59
- package/src/components/AgentCard.tsx +4 -4
- package/src/components/AgentLogPreview.tsx +2 -38
- package/src/components/App.tsx +441 -2624
- package/src/components/CliToolHarness.test.tsx +83 -0
- package/src/components/CliToolHarness.tsx +292 -0
- package/src/components/CoordinatorPanel.tsx +13 -6
- package/src/components/LogViewer.tsx +2 -42
- package/src/components/ProviderAuthFlow.tsx +201 -81
- package/src/components/ProvisioningProgress.tsx +1 -1
- package/src/components/ReactionChips.tsx +2 -1
- package/src/components/SpawnModal.test.tsx +51 -18
- package/src/components/SpawnModal.tsx +175 -207
- package/src/components/TerminalProviderSetup.tsx +1 -1
- package/src/components/ThreadPanel.tsx +2 -0
- package/src/components/WorkspaceContext.tsx +7 -19
- package/src/components/XTermLogViewer.tsx +190 -27
- package/src/components/channels/ChannelMessageList.tsx +94 -4
- package/src/components/channels/ChannelViewV1.tsx +35 -11
- package/src/components/channels/api.ts +21 -20
- package/src/components/channels/types.ts +16 -0
- package/src/components/hooks/index.ts +0 -19
- package/src/components/hooks/useMessages.test.ts +80 -0
- package/src/components/hooks/useMessages.ts +13 -4
- package/src/components/hooks/useOrchestrator.ts +1 -1
- package/src/components/hooks/usePresence.ts +45 -6
- package/src/components/hooks/useThread.ts +83 -46
- package/src/components/hooks/useTrajectory.ts +62 -5
- package/src/components/hooks/useWebSocket.test.ts +358 -0
- package/src/components/hooks/useWebSocket.ts +243 -5
- package/src/components/index.ts +2 -14
- package/src/components/layout/Header.tsx +9 -15
- package/src/components/layout/Sidebar.tsx +1 -8
- package/src/components/settings/SettingsPage.tsx +108 -47
- package/src/components/settings/index.ts +0 -3
- package/src/landing/blogData.ts +1 -1
- package/src/lib/agent-merge.test.ts +2 -2
- package/src/lib/api.ts +8 -38
- package/src/lib/identity.test.ts +139 -0
- package/src/lib/identity.ts +48 -0
- package/src/lib/relaycastMessageAdapters.test.ts +182 -0
- package/src/lib/relaycastMessageAdapters.ts +105 -0
- package/src/lib/sanitize-logs.test.ts +227 -0
- package/src/lib/sanitize-logs.ts +202 -0
- package/src/providers/AgentProvider.tsx +799 -0
- package/src/providers/ChannelProvider.tsx +528 -0
- package/src/providers/CloudWorkspaceProvider.tsx +402 -0
- package/src/providers/MessageProvider.tsx +875 -0
- package/src/providers/RelayConfigProvider.tsx +94 -0
- package/src/providers/SendProvider.tsx +497 -0
- package/src/providers/SettingsProvider.tsx +247 -0
- package/src/providers/index.ts +26 -0
- package/src/types/index.ts +10 -10
- package/out/_next/static/chunks/11-9a2993a37266dcb3.js +0 -9
- package/out/_next/static/chunks/118-ae2b650136a5a5fc.js +0 -1
- package/out/_next/static/chunks/1dd3208c-40ab0fc0f60392b8.js +0 -1
- package/out/_next/static/chunks/202-fc0763dd7488e58f.js +0 -1
- package/out/_next/static/chunks/259-83b77fa1b91ba5aa.js +0 -1
- package/out/_next/static/chunks/407-0c82986cf79c8ecb.js +0 -1
- package/out/_next/static/chunks/528-f5f676996d613c25.js +0 -2
- package/out/_next/static/chunks/663-ddb04081febc3678.js +0 -1
- package/out/_next/static/chunks/687-88b6b139a6bb0e2e.js +0 -1
- package/out/_next/static/chunks/695-51d25b1988644374.js +0 -1
- package/out/_next/static/chunks/773-54a2641043c81e55.js +0 -1
- package/out/_next/static/chunks/app/_not-found/page-6da9b72091e5b511.js +0 -1
- package/out/_next/static/chunks/app/about/page-fff7c6457683f243.js +0 -1
- package/out/_next/static/chunks/app/app/[[...slug]]/page-f7eca1b66fb4249b.js +0 -1
- package/out/_next/static/chunks/app/app/onboarding/page-129abc5da2e67971.js +0 -1
- package/out/_next/static/chunks/app/blog/go-to-bed-wake-up-to-a-finished-product/page-5d5f28fd126b692f.js +0 -1
- package/out/_next/static/chunks/app/blog/let-them-cook-multi-agent-orchestration/page-b194f207fbd91862.js +0 -1
- package/out/_next/static/chunks/app/blog/page-b9bd9d8703fca76a.js +0 -1
- package/out/_next/static/chunks/app/careers/page-a4bd8d5f4de8f4eb.js +0 -1
- package/out/_next/static/chunks/app/changelog/page-9a1f6ad1743d63c5.js +0 -1
- package/out/_next/static/chunks/app/cloud/link/page-0844c5699b027c3b.js +0 -1
- package/out/_next/static/chunks/app/complete-profile/page-39ed5a67916beb87.js +0 -1
- package/out/_next/static/chunks/app/connect-repos/page-297eddee0c39f2a3.js +0 -1
- package/out/_next/static/chunks/app/contact/page-3c1dd8690217fade.js +0 -1
- package/out/_next/static/chunks/app/docs/page-1875e981f2c3fd13.js +0 -1
- package/out/_next/static/chunks/app/history/page-2d5c5695c9e8b40c.js +0 -1
- package/out/_next/static/chunks/app/layout-0a4b99656da25511.js +0 -1
- package/out/_next/static/chunks/app/login/page-f69c076f5a6fc520.js +0 -1
- package/out/_next/static/chunks/app/metrics/page-bebbee055669a17e.js +0 -1
- package/out/_next/static/chunks/app/page-0ee604f7070d14c0.js +0 -1
- package/out/_next/static/chunks/app/pricing/page-eeae7d594af333b6.js +0 -1
- package/out/_next/static/chunks/app/providers/setup/[provider]/page-daf9b3e05e77ae19.js +0 -1
- package/out/_next/static/chunks/app/security/page-cd562730fe84a0a2.js +0 -1
- package/out/_next/static/chunks/app/signup/page-c242ca08101a84ff.js +0 -1
- package/out/_next/static/chunks/app/terms/page-c7001720e7941dc6.js +0 -1
- package/out/_next/static/chunks/framework-3664cab31236a9fa.js +0 -1
- package/out/_next/static/chunks/main-app-7f73a939a312a228.js +0 -1
- package/out/_next/static/chunks/pages/_app-10a93ab5b7c32eb3.js +0 -1
- package/out/_next/static/chunks/pages/_error-2d792b2a41857be4.js +0 -1
- package/out/_next/static/css/8968d98ed4c4d33f.css +0 -1
- package/src/components/BillingResult.tsx +0 -447
- package/src/components/CloudSessionProvider.tsx +0 -130
- package/src/components/SessionExpiredModal.tsx +0 -128
- package/src/components/WorkspaceStatusIndicator.tsx +0 -396
- package/src/components/hooks/useSession.ts +0 -209
- package/src/components/hooks/useWorkspaceMembers.ts +0 -132
- package/src/components/hooks/useWorkspaceStatus.ts +0 -237
- package/src/components/settings/BillingSettingsPanel.tsx +0 -564
- package/src/components/settings/TeamSettingsPanel.tsx +0 -560
- package/src/components/settings/WorkspaceSettingsPanel.tsx +0 -1368
- package/src/lib/cloudApi.ts +0 -893
- /package/out/_next/static/{IxfA6RZu4trcsEMYlkQra → g3G0LMdB7lxcrU5mdM54m}/_ssgManifest.js +0 -0
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Onboarding Page - Dedicated route for new users and post-deletion flow
|
|
3
3
|
*
|
|
4
|
-
* This page
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* URL params:
|
|
10
|
-
* - reason=deleted: User just deleted a workspace
|
|
11
|
-
* - reason=new: First-time user (default)
|
|
4
|
+
* This page now supports provider-first onboarding after GitHub connection:
|
|
5
|
+
* 1. Determine current onboarding step via /api/onboarding/next-step
|
|
6
|
+
* 2. Connect an AI provider (API key or CLI auth)
|
|
7
|
+
* 3. Auto-create and provision workspace
|
|
12
8
|
*/
|
|
13
9
|
|
|
14
10
|
'use client';
|
|
15
11
|
|
|
16
|
-
import React, {
|
|
12
|
+
import React, { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
17
13
|
import { useSearchParams } from 'next/navigation';
|
|
18
14
|
import { LogoIcon } from '../../../components/Logo';
|
|
15
|
+
import { ProvisioningProgress } from '../../../components/ProvisioningProgress';
|
|
19
16
|
|
|
20
17
|
interface Repository {
|
|
21
18
|
id: string;
|
|
@@ -26,28 +23,105 @@ interface Repository {
|
|
|
26
23
|
hasNangoConnection: boolean;
|
|
27
24
|
}
|
|
28
25
|
|
|
26
|
+
interface NextStepResponse {
|
|
27
|
+
nextStep?: string;
|
|
28
|
+
selectedRepo?: string;
|
|
29
|
+
repositoryFullName?: string;
|
|
30
|
+
repository?: {
|
|
31
|
+
fullName?: string;
|
|
32
|
+
};
|
|
33
|
+
connectedProviders?: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function getOnboardingNextStep(): Promise<NextStepResponse> {
|
|
37
|
+
const response = await fetch('/api/onboarding/next-step', {
|
|
38
|
+
credentials: 'include',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new Error('Failed to fetch onboarding next step');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (await response.json()) as NextStepResponse;
|
|
46
|
+
}
|
|
47
|
+
|
|
29
48
|
type OnboardingReason = 'new' | 'deleted';
|
|
49
|
+
type ProviderId = 'anthropic' | 'openai' | 'google' | 'cursor' | 'opencode' | 'droid';
|
|
50
|
+
type AuthMode = 'api_key' | 'cli';
|
|
51
|
+
type WorkspaceLifecycleState = 'idle' | 'provisioning' | 'running' | 'error';
|
|
30
52
|
|
|
31
|
-
// Analytics event types for onboarding funnel
|
|
32
53
|
type OnboardingEvent =
|
|
33
54
|
| 'onboarding_page_view'
|
|
34
55
|
| 'onboarding_repo_selected'
|
|
35
56
|
| 'onboarding_workspace_created'
|
|
36
|
-
| 'onboarding_connect_repos_clicked'
|
|
37
|
-
|
|
57
|
+
| 'onboarding_connect_repos_clicked';
|
|
58
|
+
|
|
59
|
+
interface ProviderOption {
|
|
60
|
+
id: ProviderId;
|
|
61
|
+
label: string;
|
|
62
|
+
description: string;
|
|
63
|
+
color: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const PROVIDERS: ProviderOption[] = [
|
|
67
|
+
{
|
|
68
|
+
id: 'anthropic',
|
|
69
|
+
label: 'Anthropic',
|
|
70
|
+
description: 'Claude models via Anthropic',
|
|
71
|
+
color: '#D97757',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'openai',
|
|
75
|
+
label: 'OpenAI',
|
|
76
|
+
description: 'Codex and GPT models',
|
|
77
|
+
color: '#10A37F',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
id: 'cursor',
|
|
81
|
+
label: 'Cursor',
|
|
82
|
+
description: 'Cursor AI editor',
|
|
83
|
+
color: '#7C3AED',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: 'google',
|
|
87
|
+
label: 'Google',
|
|
88
|
+
description: 'Gemini models via Google AI',
|
|
89
|
+
color: '#4285F4',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: 'opencode',
|
|
93
|
+
label: 'OpenCode',
|
|
94
|
+
description: 'Coming soon',
|
|
95
|
+
color: '#6B7280',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
id: 'droid',
|
|
99
|
+
label: 'Droid',
|
|
100
|
+
description: 'Coming soon',
|
|
101
|
+
color: '#6B7280',
|
|
102
|
+
},
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
const PROVIDER_STATUS_NAMES: Record<ProviderId, string[]> = {
|
|
106
|
+
anthropic: ['anthropic'],
|
|
107
|
+
openai: ['openai', 'codex'],
|
|
108
|
+
cursor: ['cursor'],
|
|
109
|
+
google: ['google'],
|
|
110
|
+
opencode: ['opencode'],
|
|
111
|
+
droid: ['droid'],
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
function sleep(ms: number): Promise<void> {
|
|
116
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
117
|
+
}
|
|
38
118
|
|
|
39
|
-
// Simple analytics hook - can be extended to integrate with actual analytics service
|
|
40
119
|
function useOnboardingAnalytics() {
|
|
41
120
|
const trackEvent = useCallback((event: OnboardingEvent, properties?: Record<string, unknown>) => {
|
|
42
|
-
// Log to console in development
|
|
43
121
|
if (process.env.NODE_ENV === 'development') {
|
|
44
122
|
console.log('[Onboarding Analytics]', event, properties);
|
|
45
123
|
}
|
|
46
124
|
|
|
47
|
-
// TODO: Integrate with actual analytics service (e.g., Posthog, Mixpanel, etc.)
|
|
48
|
-
// Example: posthog.capture(event, properties);
|
|
49
|
-
|
|
50
|
-
// For now, send to a hypothetical analytics endpoint
|
|
51
125
|
try {
|
|
52
126
|
fetch('/api/analytics/track', {
|
|
53
127
|
method: 'POST',
|
|
@@ -55,10 +129,10 @@ function useOnboardingAnalytics() {
|
|
|
55
129
|
credentials: 'include',
|
|
56
130
|
body: JSON.stringify({ event, properties, timestamp: Date.now() }),
|
|
57
131
|
}).catch(() => {
|
|
58
|
-
//
|
|
132
|
+
// Analytics should never block onboarding
|
|
59
133
|
});
|
|
60
134
|
} catch {
|
|
61
|
-
//
|
|
135
|
+
// Analytics should never block onboarding
|
|
62
136
|
}
|
|
63
137
|
}, []);
|
|
64
138
|
|
|
@@ -70,27 +144,282 @@ function OnboardingContent() {
|
|
|
70
144
|
const reason = (searchParams.get('reason') as OnboardingReason) || 'new';
|
|
71
145
|
|
|
72
146
|
const [repos, setRepos] = useState<Repository[]>([]);
|
|
147
|
+
const [selectedRepos, setSelectedRepos] = useState<Set<string>>(new Set());
|
|
148
|
+
const [nextStep, setNextStep] = useState<string | null>(null);
|
|
149
|
+
const [selectedProvider, setSelectedProvider] = useState<ProviderId>('anthropic');
|
|
150
|
+
const [authMode, setAuthMode] = useState<AuthMode>('api_key');
|
|
151
|
+
const [apiKey, setApiKey] = useState('');
|
|
152
|
+
const [cliCommand, setCliCommand] = useState<string | null>(null);
|
|
153
|
+
|
|
154
|
+
const [connectedProviders, setConnectedProviders] = useState<Set<ProviderId>>(new Set());
|
|
155
|
+
|
|
73
156
|
const [isLoading, setIsLoading] = useState(true);
|
|
74
|
-
const [
|
|
157
|
+
const [isSubmittingProvider, setIsSubmittingProvider] = useState(false);
|
|
158
|
+
const [isPollingCli, setIsPollingCli] = useState(false);
|
|
159
|
+
const [isCreatingWorkspace, setIsCreatingWorkspace] = useState(false);
|
|
160
|
+
|
|
161
|
+
const [workspaceState, setWorkspaceState] = useState<WorkspaceLifecycleState>('idle');
|
|
162
|
+
const [workspaceId, setWorkspaceId] = useState<string | null>(null);
|
|
163
|
+
const [provisioningStage, setProvisioningStage] = useState<string | null>(null);
|
|
164
|
+
|
|
75
165
|
const [error, setError] = useState<string | null>(null);
|
|
76
166
|
const [csrfToken, setCsrfToken] = useState<string | null>(null);
|
|
167
|
+
const [providerFeedback, setProviderFeedback] = useState<{ type: 'success' | 'error' | 'info'; text: string } | null>(null);
|
|
168
|
+
const [copied, setCopied] = useState(false);
|
|
169
|
+
|
|
170
|
+
const cliPollingRef = useRef(false);
|
|
171
|
+
const workspacePollingRef = useRef(false);
|
|
77
172
|
|
|
78
173
|
const { trackEvent } = useOnboardingAnalytics();
|
|
79
174
|
|
|
80
|
-
|
|
175
|
+
const selectedProviderMeta = useMemo(
|
|
176
|
+
() => PROVIDERS.find((provider) => provider.id === selectedProvider) || PROVIDERS[0],
|
|
177
|
+
[selectedProvider]
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const buildHeaders = useCallback(
|
|
181
|
+
(includeContentType = true): Record<string, string> => {
|
|
182
|
+
const headers: Record<string, string> = includeContentType ? { 'Content-Type': 'application/json' } : {};
|
|
183
|
+
if (csrfToken) {
|
|
184
|
+
headers['X-CSRF-Token'] = csrfToken;
|
|
185
|
+
}
|
|
186
|
+
return headers;
|
|
187
|
+
},
|
|
188
|
+
[csrfToken]
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const resolveSelectedRepos = useCallback((repositories: Repository[], stepData: NextStepResponse | null): Set<string> => {
|
|
192
|
+
const stepRepo =
|
|
193
|
+
stepData?.selectedRepo ||
|
|
194
|
+
stepData?.repositoryFullName ||
|
|
195
|
+
stepData?.repository?.fullName ||
|
|
196
|
+
'';
|
|
197
|
+
|
|
198
|
+
if (stepRepo && repositories.some((repo) => repo.fullName === stepRepo)) {
|
|
199
|
+
return new Set([stepRepo]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return repositories.length > 0 ? new Set([repositories[0].fullName]) : new Set();
|
|
203
|
+
}, []);
|
|
204
|
+
|
|
205
|
+
const refreshNextStep = useCallback(async (): Promise<NextStepResponse | null> => {
|
|
206
|
+
try {
|
|
207
|
+
const data = (await getOnboardingNextStep()) as NextStepResponse;
|
|
208
|
+
if (typeof data.nextStep === 'string') {
|
|
209
|
+
setNextStep(data.nextStep);
|
|
210
|
+
}
|
|
211
|
+
return data;
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}, []);
|
|
216
|
+
|
|
217
|
+
const verifyProviderConnected = useCallback(
|
|
218
|
+
async (statusWorkspaceId?: string): Promise<boolean> => {
|
|
219
|
+
if (statusWorkspaceId) {
|
|
220
|
+
try {
|
|
221
|
+
const statusRes = await fetch(`/api/auth/ssh/status/${statusWorkspaceId}`, {
|
|
222
|
+
credentials: 'include',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (statusRes.ok) {
|
|
226
|
+
const statusData = (await statusRes.json()) as {
|
|
227
|
+
providers?: Array<{ name: string; status: string }>;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const connectedFromStatus = PROVIDER_STATUS_NAMES[selectedProvider].some((providerName) =>
|
|
231
|
+
statusData.providers?.some((provider) => provider.name === providerName && provider.status === 'connected')
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (connectedFromStatus) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// Fall through to onboarding next-step check.
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const nextStepData = await refreshNextStep();
|
|
244
|
+
if (!nextStepData) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (nextStepData.nextStep && nextStepData.nextStep !== 'connect_ai_provider') {
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const connectedProviders = Array.isArray(nextStepData.connectedProviders)
|
|
253
|
+
? nextStepData.connectedProviders
|
|
254
|
+
: [];
|
|
255
|
+
|
|
256
|
+
return PROVIDER_STATUS_NAMES[selectedProvider].some((providerName) => connectedProviders.includes(providerName));
|
|
257
|
+
},
|
|
258
|
+
[refreshNextStep, selectedProvider]
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const pollWorkspaceUntilRunning = useCallback(async (createdWorkspaceId: string): Promise<void> => {
|
|
262
|
+
workspacePollingRef.current = true;
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const maxAttempts = 150; // 5 minutes at 2s intervals
|
|
266
|
+
let attempts = 0;
|
|
267
|
+
let consecutiveErrors = 0;
|
|
268
|
+
|
|
269
|
+
while (workspacePollingRef.current && attempts < maxAttempts) {
|
|
270
|
+
try {
|
|
271
|
+
const statusRes = await fetch(`/api/workspaces/${createdWorkspaceId}/status`, {
|
|
272
|
+
credentials: 'include',
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
if (!statusRes.ok) {
|
|
276
|
+
consecutiveErrors++;
|
|
277
|
+
console.warn(`[onboarding] Status poll returned ${statusRes.status} (attempt ${consecutiveErrors})`);
|
|
278
|
+
if (consecutiveErrors >= 5) {
|
|
279
|
+
throw new Error(`Workspace status check failed (HTTP ${statusRes.status})`);
|
|
280
|
+
}
|
|
281
|
+
await sleep(2000);
|
|
282
|
+
attempts += 1;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
consecutiveErrors = 0;
|
|
287
|
+
|
|
288
|
+
const statusData = (await statusRes.json()) as {
|
|
289
|
+
status?: string;
|
|
290
|
+
errorMessage?: string;
|
|
291
|
+
provisioning?: { stage?: string | null };
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
if (statusData.provisioning?.stage) {
|
|
295
|
+
setProvisioningStage(statusData.provisioning.stage);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (statusData.status === 'running') {
|
|
299
|
+
setWorkspaceState('running');
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (statusData.status === 'error' || statusData.status === 'stopped') {
|
|
304
|
+
throw new Error(statusData.errorMessage || `Workspace ${statusData.status === 'stopped' ? 'stopped unexpectedly' : 'provisioning failed'}`);
|
|
305
|
+
}
|
|
306
|
+
} catch (fetchErr) {
|
|
307
|
+
// Re-throw our intentional errors
|
|
308
|
+
if (fetchErr instanceof Error && (
|
|
309
|
+
fetchErr.message.includes('provisioning failed') ||
|
|
310
|
+
fetchErr.message.includes('stopped unexpectedly') ||
|
|
311
|
+
fetchErr.message.includes('status check failed')
|
|
312
|
+
)) {
|
|
313
|
+
throw fetchErr;
|
|
314
|
+
}
|
|
315
|
+
// Network errors — keep trying
|
|
316
|
+
consecutiveErrors++;
|
|
317
|
+
console.warn(`[onboarding] Status poll network error (attempt ${consecutiveErrors}):`, fetchErr);
|
|
318
|
+
if (consecutiveErrors >= 5) {
|
|
319
|
+
throw new Error('Unable to reach workspace status endpoint');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
await sleep(2000);
|
|
324
|
+
attempts += 1;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
throw new Error('Workspace provisioning timed out after 5 minutes.');
|
|
328
|
+
} finally {
|
|
329
|
+
workspacePollingRef.current = false;
|
|
330
|
+
}
|
|
331
|
+
}, []);
|
|
332
|
+
|
|
333
|
+
const startWorkspaceCreation = useCallback(
|
|
334
|
+
async (repoNames?: string[]) => {
|
|
335
|
+
const repositories = repoNames && repoNames.length > 0 ? repoNames : Array.from(selectedRepos);
|
|
336
|
+
|
|
337
|
+
if (repositories.length === 0 || isCreatingWorkspace || workspaceState === 'provisioning' || workspaceState === 'running') {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
setError(null);
|
|
342
|
+
setWorkspaceState('provisioning');
|
|
343
|
+
setProvisioningStage(null);
|
|
344
|
+
setIsCreatingWorkspace(true);
|
|
345
|
+
|
|
346
|
+
trackEvent('onboarding_repo_selected', { repositories });
|
|
347
|
+
|
|
348
|
+
try {
|
|
349
|
+
const res = await fetch('/api/workspaces/quick', {
|
|
350
|
+
method: 'POST',
|
|
351
|
+
credentials: 'include',
|
|
352
|
+
headers: buildHeaders(),
|
|
353
|
+
body: JSON.stringify({ repositories }),
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const data = await res.json();
|
|
357
|
+
|
|
358
|
+
if (!res.ok) {
|
|
359
|
+
throw new Error(data.error || 'Failed to create workspace');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (!data.workspaceId || typeof data.workspaceId !== 'string') {
|
|
363
|
+
throw new Error('Workspace created but no workspace ID was returned');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
setWorkspaceId(data.workspaceId);
|
|
367
|
+
await pollWorkspaceUntilRunning(data.workspaceId);
|
|
368
|
+
|
|
369
|
+
trackEvent('onboarding_workspace_created', {
|
|
370
|
+
workspaceId: data.workspaceId,
|
|
371
|
+
repositories,
|
|
372
|
+
});
|
|
373
|
+
} catch (err) {
|
|
374
|
+
setWorkspaceState('error');
|
|
375
|
+
setError(err instanceof Error ? err.message : 'Failed to create workspace');
|
|
376
|
+
} finally {
|
|
377
|
+
setIsCreatingWorkspace(false);
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
[selectedRepos, isCreatingWorkspace, workspaceState, trackEvent, buildHeaders, pollWorkspaceUntilRunning]
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const pollForCliCompletion = useCallback(
|
|
384
|
+
async (statusWorkspaceId?: string): Promise<boolean> => {
|
|
385
|
+
cliPollingRef.current = true;
|
|
386
|
+
setIsPollingCli(true);
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
const maxAttempts = 120; // 10 minutes at 5s intervals
|
|
390
|
+
let attempts = 0;
|
|
391
|
+
|
|
392
|
+
while (cliPollingRef.current && attempts < maxAttempts) {
|
|
393
|
+
const connected = await verifyProviderConnected(statusWorkspaceId);
|
|
394
|
+
if (connected) {
|
|
395
|
+
setProviderFeedback({ type: 'success', text: `${selectedProviderMeta.label} connected successfully.` });
|
|
396
|
+
setNextStep('create_workspace');
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
await sleep(5000);
|
|
401
|
+
attempts += 1;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return false;
|
|
405
|
+
} finally {
|
|
406
|
+
cliPollingRef.current = false;
|
|
407
|
+
setIsPollingCli(false);
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
[selectedProviderMeta.label, verifyProviderConnected]
|
|
411
|
+
);
|
|
412
|
+
|
|
81
413
|
useEffect(() => {
|
|
82
414
|
const init = async () => {
|
|
83
415
|
try {
|
|
84
|
-
// Check session
|
|
85
416
|
const sessionRes = await fetch('/api/auth/session', { credentials: 'include' });
|
|
86
417
|
|
|
87
418
|
if (sessionRes.status === 404) {
|
|
88
|
-
// Local mode - redirect to main app
|
|
89
419
|
window.location.href = '/app';
|
|
90
420
|
return;
|
|
91
421
|
}
|
|
92
422
|
|
|
93
|
-
// Capture CSRF token
|
|
94
423
|
const token = sessionRes.headers.get('X-CSRF-Token');
|
|
95
424
|
if (token) {
|
|
96
425
|
setCsrfToken(token);
|
|
@@ -103,83 +432,266 @@ function OnboardingContent() {
|
|
|
103
432
|
return;
|
|
104
433
|
}
|
|
105
434
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const workspacesData = await workspacesRes.json();
|
|
110
|
-
if ((workspacesData.workspaces || []).length > 0) {
|
|
111
|
-
// User has workspaces, redirect to main app
|
|
112
|
-
window.location.href = '/app';
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
435
|
+
// Note: we intentionally do NOT redirect to /app if workspaces exist.
|
|
436
|
+
// DashboardPageClient redirects here when no running workspaces, so redirecting
|
|
437
|
+
// back would cause an infinite loop. The onboarding page handles all states.
|
|
116
438
|
|
|
117
|
-
// Fetch repos
|
|
118
439
|
const reposRes = await fetch('/api/github-app/repos', { credentials: 'include' });
|
|
440
|
+
const repositories: Repository[] = [];
|
|
441
|
+
|
|
119
442
|
if (reposRes.ok) {
|
|
120
443
|
const reposData = await reposRes.json();
|
|
121
|
-
|
|
444
|
+
repositories.push(...(reposData.repositories || []));
|
|
122
445
|
}
|
|
123
446
|
|
|
124
|
-
|
|
125
|
-
|
|
447
|
+
setRepos(repositories);
|
|
448
|
+
|
|
449
|
+
let nextStepData: NextStepResponse | null = null;
|
|
450
|
+
try {
|
|
451
|
+
nextStepData = (await getOnboardingNextStep()) as NextStepResponse;
|
|
452
|
+
} catch {
|
|
453
|
+
nextStepData = null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const calculatedStep =
|
|
457
|
+
nextStepData?.nextStep ||
|
|
458
|
+
(repositories.length > 0 ? 'connect_ai_provider' : 'connect_repos');
|
|
459
|
+
|
|
460
|
+
setNextStep(calculatedStep);
|
|
461
|
+
setSelectedRepos(resolveSelectedRepos(repositories, nextStepData));
|
|
462
|
+
|
|
463
|
+
// Initialize connected providers from API response
|
|
464
|
+
if (Array.isArray(nextStepData?.connectedProviders)) {
|
|
465
|
+
const providerMap: Record<string, ProviderId> = {
|
|
466
|
+
anthropic: 'anthropic',
|
|
467
|
+
openai: 'openai',
|
|
468
|
+
google: 'google',
|
|
469
|
+
cursor: 'cursor',
|
|
470
|
+
};
|
|
471
|
+
const initialConnected = new Set<ProviderId>();
|
|
472
|
+
for (const p of nextStepData.connectedProviders) {
|
|
473
|
+
const mapped = providerMap[p];
|
|
474
|
+
if (mapped) initialConnected.add(mapped);
|
|
475
|
+
}
|
|
476
|
+
setConnectedProviders(initialConnected);
|
|
477
|
+
}
|
|
126
478
|
|
|
479
|
+
trackEvent('onboarding_page_view', { reason, nextStep: calculatedStep });
|
|
127
480
|
setIsLoading(false);
|
|
128
481
|
} catch (err) {
|
|
129
|
-
console.error('Onboarding init error:', err);
|
|
130
482
|
setError(err instanceof Error ? err.message : 'Failed to initialize');
|
|
131
483
|
setIsLoading(false);
|
|
132
484
|
}
|
|
133
485
|
};
|
|
134
486
|
|
|
135
487
|
init();
|
|
488
|
+
|
|
489
|
+
return () => {
|
|
490
|
+
cliPollingRef.current = false;
|
|
491
|
+
workspacePollingRef.current = false;
|
|
492
|
+
};
|
|
493
|
+
}, [reason, resolveSelectedRepos, trackEvent]);
|
|
494
|
+
|
|
495
|
+
useEffect(() => {
|
|
496
|
+
if (
|
|
497
|
+
!isLoading &&
|
|
498
|
+
repos.length > 0 &&
|
|
499
|
+
selectedRepos.size > 0 &&
|
|
500
|
+
nextStep !== null &&
|
|
501
|
+
nextStep !== 'connect_ai_provider' &&
|
|
502
|
+
nextStep !== 'provider_connected' &&
|
|
503
|
+
!workspaceId &&
|
|
504
|
+
workspaceState === 'idle' &&
|
|
505
|
+
!isCreatingWorkspace
|
|
506
|
+
) {
|
|
507
|
+
startWorkspaceCreation(Array.from(selectedRepos));
|
|
508
|
+
}
|
|
509
|
+
}, [
|
|
510
|
+
isLoading,
|
|
511
|
+
repos.length,
|
|
512
|
+
selectedRepos,
|
|
513
|
+
nextStep,
|
|
514
|
+
workspaceId,
|
|
515
|
+
workspaceState,
|
|
516
|
+
isCreatingWorkspace,
|
|
517
|
+
startWorkspaceCreation,
|
|
518
|
+
]);
|
|
519
|
+
|
|
520
|
+
const handleConnectRepos = useCallback(() => {
|
|
521
|
+
trackEvent('onboarding_connect_repos_clicked', { reason });
|
|
522
|
+
window.location.href = '/connect-repos';
|
|
136
523
|
}, [reason, trackEvent]);
|
|
137
524
|
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
525
|
+
const handleConnectApiKey = useCallback(async () => {
|
|
526
|
+
if (!apiKey.trim()) {
|
|
527
|
+
setProviderFeedback({ type: 'error', text: 'Please enter an API key.' });
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
141
530
|
|
|
142
|
-
|
|
531
|
+
if (selectedRepos.size === 0) {
|
|
532
|
+
setProviderFeedback({ type: 'error', text: 'Select at least one repository before connecting a provider.' });
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
setIsSubmittingProvider(true);
|
|
537
|
+
setProviderFeedback(null);
|
|
538
|
+
setError(null);
|
|
143
539
|
|
|
144
540
|
try {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
541
|
+
const providerCandidates = selectedProvider === 'openai' ? ['openai', 'codex'] : [selectedProvider];
|
|
542
|
+
let lastError = 'Failed to connect provider';
|
|
543
|
+
let connected = false;
|
|
544
|
+
|
|
545
|
+
for (const providerName of providerCandidates) {
|
|
546
|
+
const res = await fetch(`/api/providers/${providerName}/api-key`, {
|
|
547
|
+
method: 'POST',
|
|
548
|
+
credentials: 'include',
|
|
549
|
+
headers: buildHeaders(),
|
|
550
|
+
body: JSON.stringify({ apiKey: apiKey.trim() }),
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
if (res.ok) {
|
|
554
|
+
connected = true;
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const data = await res.json().catch(() => ({}));
|
|
559
|
+
lastError = data.error || data.message || `Failed to connect ${selectedProviderMeta.label}`;
|
|
148
560
|
}
|
|
149
561
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
562
|
+
if (!connected) {
|
|
563
|
+
throw new Error(lastError);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
setApiKey('');
|
|
567
|
+
setAuthMode('api_key');
|
|
568
|
+
setConnectedProviders(prev => new Set([...prev, selectedProvider]));
|
|
569
|
+
setProviderFeedback({ type: 'success', text: `${selectedProviderMeta.label} connected! Add another provider or continue to create your workspace.` });
|
|
570
|
+
setNextStep('provider_connected');
|
|
571
|
+
} catch (err) {
|
|
572
|
+
setProviderFeedback({
|
|
573
|
+
type: 'error',
|
|
574
|
+
text: err instanceof Error ? err.message : 'Failed to connect provider',
|
|
155
575
|
});
|
|
576
|
+
} finally {
|
|
577
|
+
setIsSubmittingProvider(false);
|
|
578
|
+
}
|
|
579
|
+
}, [apiKey, selectedRepos, selectedProvider, selectedProviderMeta.label, buildHeaders]);
|
|
580
|
+
|
|
581
|
+
const handleStartCliAuth = useCallback(async () => {
|
|
582
|
+
if (selectedRepos.size === 0) {
|
|
583
|
+
setProviderFeedback({ type: 'error', text: 'Select at least one repository before starting provider auth.' });
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
setIsSubmittingProvider(true);
|
|
588
|
+
setProviderFeedback(null);
|
|
589
|
+
setCliCommand(null);
|
|
590
|
+
setError(null);
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
const headers = buildHeaders();
|
|
594
|
+
const providerCandidates = selectedProvider === 'openai' ? ['openai', 'codex'] : [selectedProvider];
|
|
595
|
+
|
|
596
|
+
let initData: { command?: string; commandWithUrl?: string; workspaceId?: string } | null = null;
|
|
597
|
+
let lastError = 'Failed to start CLI authentication';
|
|
598
|
+
|
|
599
|
+
for (const providerName of providerCandidates) {
|
|
600
|
+
const res = await fetch('/api/auth/ssh/init', {
|
|
601
|
+
method: 'POST',
|
|
602
|
+
credentials: 'include',
|
|
603
|
+
headers,
|
|
604
|
+
body: JSON.stringify({
|
|
605
|
+
provider: providerName,
|
|
606
|
+
mode: 'onboarding',
|
|
607
|
+
}),
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
if (res.ok) {
|
|
611
|
+
initData = (await res.json()) as { command?: string; commandWithUrl?: string; workspaceId?: string };
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
const data = await res.json().catch(() => ({}));
|
|
616
|
+
lastError = data.error || data.message || 'Failed to start CLI authentication';
|
|
617
|
+
}
|
|
156
618
|
|
|
157
|
-
|
|
619
|
+
if (!initData) {
|
|
620
|
+
throw new Error(lastError);
|
|
621
|
+
}
|
|
158
622
|
|
|
159
|
-
|
|
160
|
-
|
|
623
|
+
const rawCommand = initData.commandWithUrl || initData.command;
|
|
624
|
+
if (!rawCommand) {
|
|
625
|
+
throw new Error('Auth broker did not return a command');
|
|
161
626
|
}
|
|
162
627
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
628
|
+
const commandWithNpx = rawCommand.trim().startsWith('npx ') ? rawCommand : `npx ${rawCommand}`;
|
|
629
|
+
setCliCommand(commandWithNpx);
|
|
630
|
+
setProviderFeedback({
|
|
631
|
+
type: 'info',
|
|
632
|
+
text: 'Run the command below in your terminal. This page will update automatically when auth completes.',
|
|
166
633
|
});
|
|
167
634
|
|
|
168
|
-
|
|
169
|
-
|
|
635
|
+
const connected = await pollForCliCompletion(initData.workspaceId);
|
|
636
|
+
if (connected) {
|
|
637
|
+
setAuthMode('api_key');
|
|
638
|
+
setConnectedProviders(prev => new Set([...prev, selectedProvider]));
|
|
639
|
+
setCliCommand(null);
|
|
640
|
+
setNextStep('provider_connected');
|
|
641
|
+
} else {
|
|
642
|
+
setProviderFeedback({
|
|
643
|
+
type: 'error',
|
|
644
|
+
text: 'Still waiting for authentication. Complete the CLI flow, then click "Done".',
|
|
645
|
+
});
|
|
646
|
+
}
|
|
170
647
|
} catch (err) {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
648
|
+
setProviderFeedback({
|
|
649
|
+
type: 'error',
|
|
650
|
+
text: err instanceof Error ? err.message : 'Failed to start CLI authentication',
|
|
651
|
+
});
|
|
652
|
+
} finally {
|
|
653
|
+
setIsSubmittingProvider(false);
|
|
174
654
|
}
|
|
175
|
-
}, [
|
|
655
|
+
}, [buildHeaders, selectedProvider, selectedRepos, pollForCliCompletion]);
|
|
176
656
|
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
657
|
+
const switchToCliMode = useCallback(() => {
|
|
658
|
+
setAuthMode('cli');
|
|
659
|
+
if (!cliCommand && !isSubmittingProvider && selectedRepos.size > 0) {
|
|
660
|
+
handleStartCliAuth();
|
|
661
|
+
}
|
|
662
|
+
}, [cliCommand, isSubmittingProvider, selectedRepos, handleStartCliAuth]);
|
|
663
|
+
|
|
664
|
+
const handleCliDone = useCallback(async () => {
|
|
665
|
+
setIsSubmittingProvider(true);
|
|
666
|
+
try {
|
|
667
|
+
const connected = await verifyProviderConnected();
|
|
668
|
+
if (!connected) {
|
|
669
|
+
setProviderFeedback({
|
|
670
|
+
type: 'error',
|
|
671
|
+
text: 'Provider is not connected yet. Complete the CLI authentication first.',
|
|
672
|
+
});
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
setAuthMode('api_key');
|
|
677
|
+
setConnectedProviders(prev => new Set([...prev, selectedProvider]));
|
|
678
|
+
setCliCommand(null);
|
|
679
|
+
setProviderFeedback({ type: 'success', text: `${selectedProviderMeta.label} connected! Add another provider or continue to create your workspace.` });
|
|
680
|
+
setNextStep('provider_connected');
|
|
681
|
+
} finally {
|
|
682
|
+
setIsSubmittingProvider(false);
|
|
683
|
+
}
|
|
684
|
+
}, [selectedProviderMeta.label, selectedProvider, verifyProviderConnected]);
|
|
685
|
+
|
|
686
|
+
const handleCopyCommand = useCallback(async () => {
|
|
687
|
+
if (!cliCommand) {
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
await navigator.clipboard.writeText(cliCommand);
|
|
691
|
+
setCopied(true);
|
|
692
|
+
setTimeout(() => setCopied(false), 2000);
|
|
693
|
+
}, [cliCommand]);
|
|
181
694
|
|
|
182
|
-
// Loading state
|
|
183
695
|
if (isLoading) {
|
|
184
696
|
return (
|
|
185
697
|
<div className="min-h-screen bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f] flex items-center justify-center">
|
|
@@ -188,34 +700,56 @@ function OnboardingContent() {
|
|
|
188
700
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
189
701
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
190
702
|
</svg>
|
|
191
|
-
<p className="mt-4 text-text-muted">Loading...</p>
|
|
703
|
+
<p className="mt-4 text-text-muted">Loading onboarding...</p>
|
|
192
704
|
</div>
|
|
193
705
|
</div>
|
|
194
706
|
);
|
|
195
707
|
}
|
|
196
708
|
|
|
197
|
-
|
|
198
|
-
if (isCreating) {
|
|
709
|
+
if (workspaceState === 'provisioning' || isCreatingWorkspace) {
|
|
199
710
|
return (
|
|
200
|
-
<div className="min-h-screen bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f] flex items-center justify-center">
|
|
201
|
-
<div className="
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
711
|
+
<div className="min-h-screen bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f] flex items-center justify-center p-4">
|
|
712
|
+
<div className="w-full max-w-2xl">
|
|
713
|
+
<ProvisioningProgress
|
|
714
|
+
currentStage={provisioningStage}
|
|
715
|
+
isProvisioning={true}
|
|
716
|
+
workspaceName={Array.from(selectedRepos).join(', ')}
|
|
717
|
+
error={error}
|
|
718
|
+
/>
|
|
719
|
+
</div>
|
|
720
|
+
</div>
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
if (workspaceState === 'running' && workspaceId) {
|
|
725
|
+
return (
|
|
726
|
+
<div className="min-h-screen bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f] flex items-center justify-center p-4">
|
|
727
|
+
<div className="w-full max-w-xl bg-bg-primary/80 backdrop-blur-sm border border-border-subtle rounded-2xl p-8 text-center">
|
|
728
|
+
<div className="w-16 h-16 rounded-full bg-success/20 flex items-center justify-center mx-auto mb-4">
|
|
729
|
+
<svg className="w-8 h-8 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
730
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
|
731
|
+
</svg>
|
|
732
|
+
</div>
|
|
733
|
+
<h1 className="text-2xl font-bold text-white">Workspace Ready</h1>
|
|
734
|
+
<p className="mt-2 text-text-muted">
|
|
735
|
+
Your workspace is running and ready for your first agent.
|
|
736
|
+
</p>
|
|
737
|
+
<a
|
|
738
|
+
href={`/app?workspace=${workspaceId}&spawn=true`}
|
|
739
|
+
className="mt-6 inline-flex items-center gap-2 py-3 px-6 bg-gradient-to-r from-accent-cyan to-[#00b8d9] text-bg-deep font-semibold rounded-xl hover:shadow-glow-cyan transition-all"
|
|
740
|
+
>
|
|
741
|
+
Spawn your first agent
|
|
742
|
+
</a>
|
|
208
743
|
</div>
|
|
209
744
|
</div>
|
|
210
745
|
);
|
|
211
746
|
}
|
|
212
747
|
|
|
213
|
-
// Determine content based on reason
|
|
214
748
|
const isDeletedWorkspace = reason === 'deleted';
|
|
749
|
+
const showProviderStep = repos.length > 0 && (nextStep === 'connect_ai_provider' || nextStep === 'provider_connected');
|
|
215
750
|
|
|
216
751
|
return (
|
|
217
752
|
<div className="min-h-screen bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f] flex flex-col items-center justify-center p-4">
|
|
218
|
-
{/* Background grid */}
|
|
219
753
|
<div className="fixed inset-0 opacity-10 pointer-events-none">
|
|
220
754
|
<div
|
|
221
755
|
className="absolute inset-0"
|
|
@@ -228,7 +762,6 @@ function OnboardingContent() {
|
|
|
228
762
|
</div>
|
|
229
763
|
|
|
230
764
|
<div className="relative z-10 w-full max-w-2xl">
|
|
231
|
-
{/* Logo and Header */}
|
|
232
765
|
<div className="flex flex-col items-center mb-8">
|
|
233
766
|
<LogoIcon size={56} withGlow={true} />
|
|
234
767
|
<h1 className="mt-6 text-3xl font-bold text-white">
|
|
@@ -236,130 +769,279 @@ function OnboardingContent() {
|
|
|
236
769
|
</h1>
|
|
237
770
|
<p className="mt-3 text-text-muted text-center max-w-md">
|
|
238
771
|
{isDeletedWorkspace
|
|
239
|
-
? 'Your workspace
|
|
240
|
-
: '
|
|
772
|
+
? 'Your workspace was deleted. Reconnect and provision a new environment to continue.'
|
|
773
|
+
: 'Connect an AI provider, then we will automatically provision your first workspace.'}
|
|
241
774
|
</p>
|
|
242
775
|
</div>
|
|
243
776
|
|
|
244
|
-
{/* Error message */}
|
|
245
777
|
{error && (
|
|
246
778
|
<div className="mb-6 p-4 bg-error/10 border border-error/20 rounded-xl">
|
|
247
779
|
<p className="text-error text-center">{error}</p>
|
|
248
780
|
</div>
|
|
249
781
|
)}
|
|
250
782
|
|
|
251
|
-
{/* Main content card */}
|
|
252
783
|
<div className="bg-bg-primary/80 backdrop-blur-sm border border-border-subtle rounded-2xl p-8">
|
|
253
|
-
{
|
|
254
|
-
{!isDeletedWorkspace && (
|
|
784
|
+
{repos.length > 0 && (
|
|
255
785
|
<div className="flex items-center justify-center gap-3 mb-8">
|
|
256
|
-
<
|
|
257
|
-
<div className="w-8 h-8 rounded-full bg-accent-cyan flex items-center justify-center text-bg-deep font-semibold text-sm">
|
|
258
|
-
1
|
|
259
|
-
</div>
|
|
260
|
-
<span className="text-white font-medium">Select Repository</span>
|
|
261
|
-
</div>
|
|
786
|
+
<StepBadge label="Repository" active={selectedRepos.size > 0} done={selectedRepos.size > 0} />
|
|
262
787
|
<div className="w-12 h-px bg-border-subtle" />
|
|
263
|
-
<
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
</div>
|
|
788
|
+
<StepBadge
|
|
789
|
+
label="Provider"
|
|
790
|
+
active={showProviderStep}
|
|
791
|
+
done={nextStep !== 'connect_ai_provider'}
|
|
792
|
+
/>
|
|
269
793
|
<div className="w-12 h-px bg-border-subtle" />
|
|
270
|
-
<
|
|
271
|
-
<div className="w-8 h-8 rounded-full bg-bg-tertiary border border-border-subtle flex items-center justify-center text-text-muted font-semibold text-sm">
|
|
272
|
-
3
|
|
273
|
-
</div>
|
|
274
|
-
<span className="text-text-muted">Start Building</span>
|
|
275
|
-
</div>
|
|
794
|
+
<StepBadge label="Provision" active={false} done={false} />
|
|
276
795
|
</div>
|
|
277
796
|
)}
|
|
278
797
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
</h2>
|
|
282
|
-
<p className="text-text-muted mb-6">
|
|
283
|
-
{isDeletedWorkspace
|
|
284
|
-
? 'Select a repository to create a new workspace for your AI agents.'
|
|
285
|
-
: 'Your workspace will be set up with this repository. You can add more repos later.'}
|
|
286
|
-
</p>
|
|
287
|
-
|
|
288
|
-
{repos.length > 0 ? (
|
|
289
|
-
<div className="space-y-3">
|
|
290
|
-
{repos.map((repo) => (
|
|
291
|
-
<button
|
|
292
|
-
key={repo.id}
|
|
293
|
-
onClick={() => handleCreateWorkspace(repo.fullName)}
|
|
294
|
-
disabled={isCreating}
|
|
295
|
-
className="w-full flex items-center gap-4 p-4 bg-bg-tertiary rounded-xl border border-border-subtle hover:border-accent-cyan/50 hover:bg-bg-hover transition-all text-left group disabled:opacity-50 disabled:cursor-not-allowed"
|
|
296
|
-
>
|
|
297
|
-
<div className="w-12 h-12 rounded-lg bg-bg-card border border-border-subtle flex items-center justify-center flex-shrink-0 group-hover:border-accent-cyan/30 transition-colors">
|
|
298
|
-
<svg className="w-6 h-6 text-text-muted group-hover:text-accent-cyan transition-colors" fill="currentColor" viewBox="0 0 16 16">
|
|
299
|
-
<path d="M2 2.5A2.5 2.5 0 014.5 0h8.75a.75.75 0 01.75.75v12.5a.75.75 0 01-.75.75h-2.5a.75.75 0 110-1.5h1.75v-2h-8a1 1 0 00-.714 1.7.75.75 0 01-1.072 1.05A2.495 2.495 0 012 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 011-1h8z" />
|
|
300
|
-
</svg>
|
|
301
|
-
</div>
|
|
302
|
-
<div className="flex-1 min-w-0">
|
|
303
|
-
<p className="text-white font-medium truncate group-hover:text-accent-cyan transition-colors">
|
|
304
|
-
{repo.fullName}
|
|
305
|
-
</p>
|
|
306
|
-
<p className="text-text-muted text-sm mt-0.5">
|
|
307
|
-
{repo.isPrivate ? 'Private repository' : 'Public repository'} · {repo.defaultBranch}
|
|
308
|
-
</p>
|
|
309
|
-
</div>
|
|
310
|
-
<svg className="w-5 h-5 text-text-muted group-hover:text-accent-cyan group-hover:translate-x-1 transition-all" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
311
|
-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
312
|
-
</svg>
|
|
313
|
-
</button>
|
|
314
|
-
))}
|
|
315
|
-
</div>
|
|
316
|
-
) : (
|
|
317
|
-
<div className="text-center py-12 bg-bg-tertiary rounded-xl border border-border-subtle">
|
|
318
|
-
<div className="w-16 h-16 mx-auto mb-4 bg-bg-card rounded-full flex items-center justify-center">
|
|
319
|
-
<svg className="w-8 h-8 text-text-muted" fill="currentColor" viewBox="0 0 24 24">
|
|
320
|
-
<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" />
|
|
321
|
-
</svg>
|
|
322
|
-
</div>
|
|
798
|
+
{repos.length === 0 ? (
|
|
799
|
+
<div className="text-center py-8 bg-bg-tertiary rounded-xl border border-border-subtle">
|
|
323
800
|
<h3 className="text-lg font-semibold text-white mb-2">No Repositories Connected</h3>
|
|
324
801
|
<p className="text-text-muted mb-6 max-w-sm mx-auto">
|
|
325
|
-
Connect your GitHub repositories to
|
|
802
|
+
Connect your GitHub repositories to continue onboarding.
|
|
326
803
|
</p>
|
|
327
804
|
<button
|
|
328
805
|
onClick={handleConnectRepos}
|
|
329
806
|
className="inline-flex items-center gap-2 py-3 px-6 bg-gradient-to-r from-accent-cyan to-[#00b8d9] text-bg-deep font-semibold rounded-xl hover:shadow-glow-cyan transition-all"
|
|
330
807
|
>
|
|
331
|
-
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
|
332
|
-
<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" />
|
|
333
|
-
</svg>
|
|
334
808
|
Connect GitHub
|
|
335
809
|
</button>
|
|
336
810
|
</div>
|
|
811
|
+
) : showProviderStep ? (
|
|
812
|
+
<div className="space-y-6">
|
|
813
|
+
<div>
|
|
814
|
+
<label className="block text-sm text-text-muted mb-2">Repositories</label>
|
|
815
|
+
<div className="max-h-48 overflow-y-auto space-y-2 p-3 bg-bg-tertiary border border-border-subtle rounded-xl">
|
|
816
|
+
{repos.map((repo) => (
|
|
817
|
+
<label
|
|
818
|
+
key={repo.id}
|
|
819
|
+
className="flex items-center gap-3 p-2 rounded-lg hover:bg-bg-card cursor-pointer transition-colors"
|
|
820
|
+
>
|
|
821
|
+
<input
|
|
822
|
+
type="checkbox"
|
|
823
|
+
checked={selectedRepos.has(repo.fullName)}
|
|
824
|
+
onChange={() => {
|
|
825
|
+
setSelectedRepos(prev => {
|
|
826
|
+
const next = new Set(prev);
|
|
827
|
+
if (next.has(repo.fullName)) {
|
|
828
|
+
next.delete(repo.fullName);
|
|
829
|
+
} else {
|
|
830
|
+
next.add(repo.fullName);
|
|
831
|
+
}
|
|
832
|
+
return next;
|
|
833
|
+
});
|
|
834
|
+
}}
|
|
835
|
+
className="w-4 h-4 rounded border-border-subtle text-accent-cyan focus:ring-accent-cyan/50 bg-bg-deep"
|
|
836
|
+
/>
|
|
837
|
+
<span className="text-white text-sm">{repo.fullName}</span>
|
|
838
|
+
{repo.isPrivate && (
|
|
839
|
+
<span className="text-xs text-text-muted bg-bg-deep px-1.5 py-0.5 rounded">private</span>
|
|
840
|
+
)}
|
|
841
|
+
</label>
|
|
842
|
+
))}
|
|
843
|
+
</div>
|
|
844
|
+
<p className="text-xs text-text-muted mt-1">{selectedRepos.size} selected</p>
|
|
845
|
+
</div>
|
|
846
|
+
|
|
847
|
+
<div>
|
|
848
|
+
<p className="text-sm text-text-muted mb-3">Select AI provider</p>
|
|
849
|
+
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
|
850
|
+
{PROVIDERS.map((provider) => {
|
|
851
|
+
const isConnected = connectedProviders.has(provider.id);
|
|
852
|
+
return (
|
|
853
|
+
<button
|
|
854
|
+
key={provider.id}
|
|
855
|
+
type="button"
|
|
856
|
+
onClick={() => {
|
|
857
|
+
setSelectedProvider(provider.id);
|
|
858
|
+
// Reset auth state so user can connect this provider
|
|
859
|
+
setCliCommand(null);
|
|
860
|
+
setApiKey('');
|
|
861
|
+
setProviderFeedback(null);
|
|
862
|
+
if (authMode !== 'api_key') setAuthMode('api_key');
|
|
863
|
+
}}
|
|
864
|
+
className={`p-3 rounded-xl border text-left transition-colors relative ${
|
|
865
|
+
isConnected
|
|
866
|
+
? 'border-success/50 bg-success/10'
|
|
867
|
+
: selectedProvider === provider.id
|
|
868
|
+
? 'border-accent-cyan bg-accent-cyan/10'
|
|
869
|
+
: 'border-border-subtle bg-bg-tertiary hover:border-accent-cyan/40'
|
|
870
|
+
}`}
|
|
871
|
+
>
|
|
872
|
+
<div className="flex items-center justify-between">
|
|
873
|
+
<p className="text-white font-medium">{provider.label}</p>
|
|
874
|
+
{isConnected && (
|
|
875
|
+
<span className="flex items-center gap-1 text-xs text-success font-medium">
|
|
876
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
877
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
|
|
878
|
+
</svg>
|
|
879
|
+
Connected
|
|
880
|
+
</span>
|
|
881
|
+
)}
|
|
882
|
+
</div>
|
|
883
|
+
<p className="text-xs text-text-muted mt-1">{provider.description}</p>
|
|
884
|
+
</button>
|
|
885
|
+
);
|
|
886
|
+
})}
|
|
887
|
+
</div>
|
|
888
|
+
</div>
|
|
889
|
+
|
|
890
|
+
{/* Show "Continue" button when at least one provider is connected */}
|
|
891
|
+
{connectedProviders.size > 0 && nextStep === 'provider_connected' && (
|
|
892
|
+
<div className="flex items-center justify-between p-4 bg-success/10 border border-success/30 rounded-xl">
|
|
893
|
+
<p className="text-sm text-success">
|
|
894
|
+
{connectedProviders.size} provider{connectedProviders.size > 1 ? 's' : ''} connected.
|
|
895
|
+
{' '}Add more or continue to create your workspace.
|
|
896
|
+
</p>
|
|
897
|
+
<button
|
|
898
|
+
type="button"
|
|
899
|
+
onClick={() => startWorkspaceCreation()}
|
|
900
|
+
className="px-5 py-2.5 bg-gradient-to-r from-accent-cyan to-[#00b8d9] text-bg-deep font-semibold rounded-xl hover:shadow-glow-cyan transition-all text-sm"
|
|
901
|
+
>
|
|
902
|
+
Create Workspace
|
|
903
|
+
</button>
|
|
904
|
+
</div>
|
|
905
|
+
)}
|
|
906
|
+
|
|
907
|
+
{!connectedProviders.has(selectedProvider) && (
|
|
908
|
+
<div className="flex gap-2 p-1 bg-bg-tertiary rounded-xl border border-border-subtle">
|
|
909
|
+
<button
|
|
910
|
+
type="button"
|
|
911
|
+
onClick={() => setAuthMode('api_key')}
|
|
912
|
+
className={`flex-1 py-2.5 px-4 text-sm rounded-lg transition-colors ${
|
|
913
|
+
authMode === 'api_key' ? 'bg-accent-cyan text-bg-deep font-semibold' : 'text-text-muted hover:text-white'
|
|
914
|
+
}`}
|
|
915
|
+
>
|
|
916
|
+
API Key Input
|
|
917
|
+
</button>
|
|
918
|
+
<button
|
|
919
|
+
type="button"
|
|
920
|
+
onClick={switchToCliMode}
|
|
921
|
+
className={`flex-1 py-2.5 px-4 text-sm rounded-lg transition-colors ${
|
|
922
|
+
authMode === 'cli' ? 'bg-accent-cyan text-bg-deep font-semibold' : 'text-text-muted hover:text-white'
|
|
923
|
+
}`}
|
|
924
|
+
>
|
|
925
|
+
Authenticate via CLI
|
|
926
|
+
</button>
|
|
927
|
+
</div>
|
|
928
|
+
)}
|
|
929
|
+
|
|
930
|
+
{connectedProviders.has(selectedProvider) ? null : authMode === 'api_key' ? (
|
|
931
|
+
<div className="space-y-3">
|
|
932
|
+
<label className="block text-sm text-text-muted">{selectedProviderMeta.label} API Key</label>
|
|
933
|
+
<div className="flex gap-3">
|
|
934
|
+
<input
|
|
935
|
+
type="password"
|
|
936
|
+
value={apiKey}
|
|
937
|
+
onChange={(event) => setApiKey(event.target.value)}
|
|
938
|
+
placeholder="Paste API key"
|
|
939
|
+
className="flex-1 px-4 py-3 bg-bg-tertiary border border-border-subtle rounded-xl text-white placeholder:text-text-muted focus:outline-none focus:border-accent-cyan/50"
|
|
940
|
+
/>
|
|
941
|
+
<button
|
|
942
|
+
type="button"
|
|
943
|
+
onClick={handleConnectApiKey}
|
|
944
|
+
disabled={isSubmittingProvider || !apiKey.trim()}
|
|
945
|
+
className="px-5 py-3 bg-accent-cyan text-bg-deep font-semibold rounded-xl hover:bg-accent-cyan/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
946
|
+
>
|
|
947
|
+
{isSubmittingProvider ? 'Connecting...' : 'Connect'}
|
|
948
|
+
</button>
|
|
949
|
+
</div>
|
|
950
|
+
</div>
|
|
951
|
+
) : (
|
|
952
|
+
<div className="space-y-3">
|
|
953
|
+
{cliCommand ? (
|
|
954
|
+
<div className="p-4 bg-bg-tertiary border border-border-subtle rounded-xl space-y-3">
|
|
955
|
+
<p className="text-sm text-text-muted">Run this command in your terminal:</p>
|
|
956
|
+
<code className="block px-3 py-2 bg-bg-deep rounded-lg text-sm text-white overflow-x-auto">
|
|
957
|
+
{cliCommand}
|
|
958
|
+
</code>
|
|
959
|
+
<div className="flex gap-2">
|
|
960
|
+
<button
|
|
961
|
+
type="button"
|
|
962
|
+
onClick={handleCopyCommand}
|
|
963
|
+
className={`px-3 py-2 border rounded-lg text-xs transition-colors ${
|
|
964
|
+
copied
|
|
965
|
+
? 'bg-success/20 border-success/50 text-success'
|
|
966
|
+
: 'bg-bg-card border-border-subtle text-text-muted hover:text-white hover:border-accent-cyan/50'
|
|
967
|
+
}`}
|
|
968
|
+
>
|
|
969
|
+
{copied ? 'Copied!' : 'Copy command'}
|
|
970
|
+
</button>
|
|
971
|
+
<button
|
|
972
|
+
type="button"
|
|
973
|
+
onClick={handleCliDone}
|
|
974
|
+
disabled={isSubmittingProvider}
|
|
975
|
+
className="px-3 py-2 bg-accent-cyan text-bg-deep rounded-lg text-xs font-semibold hover:bg-accent-cyan/90 disabled:opacity-50 transition-colors"
|
|
976
|
+
>
|
|
977
|
+
Done
|
|
978
|
+
</button>
|
|
979
|
+
</div>
|
|
980
|
+
</div>
|
|
981
|
+
) : (
|
|
982
|
+
<div className="flex items-center gap-3 p-4 bg-bg-tertiary border border-border-subtle rounded-xl">
|
|
983
|
+
<svg className="w-5 h-5 text-accent-cyan animate-spin" fill="none" viewBox="0 0 24 24">
|
|
984
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
985
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
986
|
+
</svg>
|
|
987
|
+
<span className="text-sm text-text-muted">Fetching CLI command...</span>
|
|
988
|
+
</div>
|
|
989
|
+
)}
|
|
990
|
+
|
|
991
|
+
{isPollingCli && (
|
|
992
|
+
<div className="flex items-center gap-2 text-sm text-success p-3 bg-success/10 border border-success/30 rounded-lg">
|
|
993
|
+
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
|
|
994
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
995
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
996
|
+
</svg>
|
|
997
|
+
<span>Waiting for authentication to complete...</span>
|
|
998
|
+
</div>
|
|
999
|
+
)}
|
|
1000
|
+
</div>
|
|
1001
|
+
)}
|
|
1002
|
+
|
|
1003
|
+
{providerFeedback && (
|
|
1004
|
+
<div
|
|
1005
|
+
className={`p-3 rounded-lg text-sm ${
|
|
1006
|
+
providerFeedback.type === 'success'
|
|
1007
|
+
? 'bg-success/10 border border-success/30 text-success'
|
|
1008
|
+
: providerFeedback.type === 'error'
|
|
1009
|
+
? 'bg-error/10 border border-error/30 text-error'
|
|
1010
|
+
: 'bg-accent-cyan/10 border border-accent-cyan/30 text-accent-cyan'
|
|
1011
|
+
}`}
|
|
1012
|
+
>
|
|
1013
|
+
{providerFeedback.text}
|
|
1014
|
+
</div>
|
|
1015
|
+
)}
|
|
1016
|
+
</div>
|
|
1017
|
+
) : (
|
|
1018
|
+
<div className="text-center py-8">
|
|
1019
|
+
<svg className="w-8 h-8 text-accent-cyan animate-spin mx-auto" fill="none" viewBox="0 0 24 24">
|
|
1020
|
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
1021
|
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
1022
|
+
</svg>
|
|
1023
|
+
<p className="mt-4 text-white font-medium">Preparing workspace provisioning...</p>
|
|
1024
|
+
<p className="mt-2 text-text-muted text-sm">You will be redirected automatically when ready.</p>
|
|
1025
|
+
</div>
|
|
337
1026
|
)}
|
|
338
1027
|
</div>
|
|
339
1028
|
|
|
340
|
-
{/* Footer navigation */}
|
|
341
1029
|
<div className="mt-8 flex justify-center gap-6 text-sm">
|
|
342
1030
|
{repos.length > 0 && (
|
|
343
|
-
<button
|
|
344
|
-
onClick={handleConnectRepos}
|
|
345
|
-
className="text-text-muted hover:text-white transition-colors"
|
|
346
|
-
>
|
|
1031
|
+
<button onClick={handleConnectRepos} className="text-text-muted hover:text-white transition-colors">
|
|
347
1032
|
Connect More Repositories
|
|
348
1033
|
</button>
|
|
349
1034
|
)}
|
|
350
|
-
<a
|
|
351
|
-
href="/app"
|
|
352
|
-
className="text-text-muted hover:text-white transition-colors"
|
|
353
|
-
>
|
|
1035
|
+
<a href="/app" className="text-text-muted hover:text-white transition-colors">
|
|
354
1036
|
Back to Dashboard
|
|
355
1037
|
</a>
|
|
356
1038
|
<button
|
|
357
1039
|
onClick={async () => {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
1040
|
+
await fetch('/api/auth/logout', {
|
|
1041
|
+
method: 'POST',
|
|
1042
|
+
credentials: 'include',
|
|
1043
|
+
headers: buildHeaders(false),
|
|
1044
|
+
});
|
|
363
1045
|
window.location.href = '/login';
|
|
364
1046
|
}}
|
|
365
1047
|
className="text-text-muted hover:text-white transition-colors"
|
|
@@ -372,7 +1054,25 @@ function OnboardingContent() {
|
|
|
372
1054
|
);
|
|
373
1055
|
}
|
|
374
1056
|
|
|
375
|
-
|
|
1057
|
+
function StepBadge({ label, active, done }: { label: string; active: boolean; done: boolean }) {
|
|
1058
|
+
return (
|
|
1059
|
+
<div className="flex items-center gap-2">
|
|
1060
|
+
<div
|
|
1061
|
+
className={`w-8 h-8 rounded-full flex items-center justify-center text-xs font-semibold ${
|
|
1062
|
+
done
|
|
1063
|
+
? 'bg-success text-bg-deep'
|
|
1064
|
+
: active
|
|
1065
|
+
? 'bg-accent-cyan text-bg-deep'
|
|
1066
|
+
: 'bg-bg-tertiary border border-border-subtle text-text-muted'
|
|
1067
|
+
}`}
|
|
1068
|
+
>
|
|
1069
|
+
{done ? '✓' : label[0]}
|
|
1070
|
+
</div>
|
|
1071
|
+
<span className={`${active || done ? 'text-white' : 'text-text-muted'} text-sm`}>{label}</span>
|
|
1072
|
+
</div>
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
|
|
376
1076
|
export default function OnboardingPage() {
|
|
377
1077
|
return (
|
|
378
1078
|
<Suspense
|