mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
Improve editor deep link handling (#46)
This commit is contained in:
parent
112c1796f6
commit
7a9a200c8b
13 changed files with 410 additions and 85 deletions
4
.env
4
.env
|
|
@ -22,7 +22,3 @@ NEXT_PUBLIC_CLERK_SIGN_IN_FORCE_REDIRECT_URL=/authorized
|
|||
NEXT_PUBLIC_CLERK_SIGN_UP_FORCE_REDIRECT_URL=/authorized
|
||||
NEXT_PUBLIC_CLERK_SIGN_IN_FALLBACK_REDIRECT_URL=/authorized
|
||||
NEXT_PUBLIC_CLERK_SIGN_UP_FALLBACK_REDIRECT_URL=/authorized
|
||||
|
||||
# Deep Links
|
||||
VSCODE_EXTENSION_BASE_URL=vscode://RooVeterinaryInc.roo-cline
|
||||
CURSOR_EXTENSION_BASE_URL=cursor://RooVeterinaryInc.roo-cline
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useSessionStorage } from 'react-use';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@clerk/nextjs';
|
||||
import { LoaderCircle, CircleCheck, CircleX } from 'lucide-react';
|
||||
|
||||
import { useAuthState } from '@/hooks/useAuthState';
|
||||
|
||||
export default function Page() {
|
||||
const { isSignedIn, orgId } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const isLoadingRef = useRef(true);
|
||||
const router = useRouter();
|
||||
const [state, setState] = useSessionStorage<string | undefined>('state');
|
||||
|
||||
const { state, ide } = useAuthState();
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof isSignedIn !== 'undefined' && isLoadingRef.current) {
|
||||
|
|
@ -25,16 +27,18 @@ export default function Page() {
|
|||
let path;
|
||||
|
||||
if (!isSignedIn) {
|
||||
path = state ? `/sign-in?state=${state}` : '/sign-in';
|
||||
path = state ? `/sign-in?state=${state}&ide=${ide}` : '/sign-in';
|
||||
} else if (!orgId) {
|
||||
path = state ? `/select-org/${state}` : '/select-org';
|
||||
path = state ? `/select-org/${state}?ide=${ide}` : '/select-org';
|
||||
} else {
|
||||
path = state ? `/extension/sign-in?state=${state}` : '/dashboard';
|
||||
path = state
|
||||
? `/extension/sign-in?state=${state}&ide=${ide}`
|
||||
: '/dashboard';
|
||||
}
|
||||
|
||||
setTimeout(() => router.push(path), 1000);
|
||||
}
|
||||
}, [router, isLoading, isSignedIn, orgId, state, setState]);
|
||||
}, [router, isLoading, isSignedIn, orgId, state, ide]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,45 +1,92 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { Card, CardContent, CardFooter } from '@/components/ui';
|
||||
import { Card, CardContent } from '@/components/ui';
|
||||
|
||||
import { VSCodeLogo } from './VSCodeLogo';
|
||||
import { VSCodeInsidersLogo } from './VSCodeInsidersLogo';
|
||||
import { CursorLogo } from './CursorLogo';
|
||||
import { WindsurfLogo } from './WindsurfLogo';
|
||||
import { TraeLogo } from './TraeLogo';
|
||||
|
||||
interface DeepLinkProps {
|
||||
vsCodeUrl: string;
|
||||
cursorUrl: string;
|
||||
editor: string;
|
||||
deepLinkUrl: string;
|
||||
}
|
||||
|
||||
export const DeepLink = ({ vsCodeUrl, cursorUrl }: DeepLinkProps) => {
|
||||
export const DeepLink = ({ editor, deepLinkUrl }: DeepLinkProps) => {
|
||||
const [redirectAttempted, setRedirectAttempted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
window.location.href = vsCodeUrl;
|
||||
window.location.href = deepLinkUrl;
|
||||
const timer = setTimeout(() => setRedirectAttempted(true), 2000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [vsCodeUrl]);
|
||||
}, [deepLinkUrl]);
|
||||
|
||||
const ides = useMemo(
|
||||
() => [
|
||||
{
|
||||
editor: 'vscode',
|
||||
title: 'Visual Studio Code',
|
||||
logo: VSCodeLogo,
|
||||
href: deepLinkUrl.replace(`${editor}://`, 'vscode://'),
|
||||
},
|
||||
{
|
||||
editor: 'vscode-insiders',
|
||||
title: 'Visual Studio Code (Insiders)',
|
||||
logo: VSCodeInsidersLogo,
|
||||
href: deepLinkUrl.replace(`${editor}://`, 'vscode-insiders://'),
|
||||
},
|
||||
{
|
||||
editor: 'cursor',
|
||||
title: 'Cursor',
|
||||
logo: CursorLogo,
|
||||
href: deepLinkUrl.replace(`${editor}://`, 'cursor://'),
|
||||
},
|
||||
{
|
||||
editor: 'windsurf',
|
||||
title: 'Windsurf',
|
||||
logo: WindsurfLogo,
|
||||
href: deepLinkUrl.replace(`${editor}://`, 'windsurf://'),
|
||||
},
|
||||
{
|
||||
editor: 'trae',
|
||||
title: 'Trae',
|
||||
logo: TraeLogo,
|
||||
href: deepLinkUrl.replace(`${editor}://`, 'trae://'),
|
||||
},
|
||||
],
|
||||
[editor, deepLinkUrl],
|
||||
);
|
||||
|
||||
const currentIde = ides.find((ide) => ide.editor === editor) ?? ides[0]!;
|
||||
|
||||
return (
|
||||
<Card className="max-w-md mx-auto">
|
||||
<Card className="max-w-lg mx-auto">
|
||||
<CardContent className="flex flex-col gap-4">
|
||||
<div>You've successfully authenticated.</div>
|
||||
<div className="flex flex-row items-center gap-4">
|
||||
<div>Open in your IDE:</div>
|
||||
<Link href={vsCodeUrl}>
|
||||
<VSCodeLogo />
|
||||
</Link>
|
||||
<Link href={cursorUrl}>
|
||||
<CursorLogo />
|
||||
<div className="flex flex-row justify-center items-center gap-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Redirecting to {currentIde.title}...
|
||||
</div>
|
||||
<Link href={deepLinkUrl} title={currentIde.title}>
|
||||
<currentIde.logo />
|
||||
</Link>
|
||||
</div>
|
||||
{redirectAttempted && (
|
||||
<div className="flex flex-row justify-center items-center gap-2">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Or, use another IDE:
|
||||
</div>
|
||||
{ides.map((ide) => (
|
||||
<Link key={ide.editor} href={ide.href} title={ide.title}>
|
||||
<ide.logo width={20} height={20} />
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="text-sm text-muted-foreground">
|
||||
{redirectAttempted
|
||||
? 'You can close this window after your IDE opens.'
|
||||
: 'Redirecting automatically...'}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
24
src/app/(centered)/extension/sign-in/TraeLogo.tsx
Normal file
24
src/app/(centered)/extension/sign-in/TraeLogo.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import type { SVGProps } from 'react';
|
||||
|
||||
type TraeLogoProps = SVGProps<SVGSVGElement>;
|
||||
|
||||
export const TraeLogo = ({
|
||||
width = '36',
|
||||
height = '36',
|
||||
...props
|
||||
}: TraeLogoProps) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M27.4343 0H0.565657C0.253253 0 0 0.253253 0 0.565657V27.4343C0 27.7467 0.253253 28 0.565657 28H27.4343C27.7467 28 28 27.7467 28 27.4343V0.565657C28 0.253253 27.7467 0 27.4343 0Z"
|
||||
fill="#FF4A36"
|
||||
/>
|
||||
<path d="M22.9121 19.8093H13.8616V22.9091H22.9121V19.8093Z" fill="white" />
|
||||
</svg>
|
||||
);
|
||||
35
src/app/(centered)/extension/sign-in/VSCodeInsidersLogo.tsx
Normal file
35
src/app/(centered)/extension/sign-in/VSCodeInsidersLogo.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import type { SVGProps } from 'react';
|
||||
|
||||
type VSCodeInsidersLogoProps = SVGProps<SVGSVGElement>;
|
||||
|
||||
export const VSCodeInsidersLogo = ({
|
||||
width = '36',
|
||||
height = '36',
|
||||
...props
|
||||
}: VSCodeInsidersLogoProps) => (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="2 2 28 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
d="M20.375,3.291a.874.874,0,0,1,1.463.647V10.25l-8.36,6.624L9.172,13.608Z"
|
||||
fill="#009a7c"
|
||||
/>
|
||||
<path
|
||||
d="M6.013,16.669,2.38,19.8A1.166,1.166,0,0,0,2.3,21.447c.025.027.05.053.077.077l1.541,1.4a1.166,1.166,0,0,0,1.489.066L9.6,19.935Z"
|
||||
fill="#009a7c"
|
||||
/>
|
||||
<path
|
||||
d="M21.838,21.749,5.412,9.007a1.165,1.165,0,0,0-1.489.066l-1.541,1.4a1.166,1.166,0,0,0-.077,1.647c.025.027.05.053.077.077l17.99,16.5a.875.875,0,0,0,1.466-.645Z"
|
||||
fill="#00b294"
|
||||
/>
|
||||
<path
|
||||
d="M23.244,29.747a1.745,1.745,0,0,1-1.989-.338A1.025,1.025,0,0,0,23,28.684V3.316a1.025,1.025,0,0,0-1.749-.725,1.745,1.745,0,0,1,1.989-.338l5.765,2.772A1.748,1.748,0,0,1,30,6.6V25.4a1.748,1.748,0,0,1-.991,1.576Z"
|
||||
fill="#24bfa5"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
56
src/app/(centered)/extension/sign-in/WindsurfLogo.tsx
Normal file
56
src/app/(centered)/extension/sign-in/WindsurfLogo.tsx
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,39 +1,53 @@
|
|||
import { redirect } from 'next/navigation';
|
||||
import { auth } from '@clerk/nextjs/server';
|
||||
|
||||
import { Env } from '@/lib/server';
|
||||
import { getSignInToken } from '@/actions/auth';
|
||||
import { EXTENSION_EDITOR, EXTENSION_URL } from '@/lib/constants';
|
||||
|
||||
import { DeepLink } from './DeepLink';
|
||||
|
||||
type Props = {
|
||||
searchParams: Promise<{ state?: string }>;
|
||||
searchParams: Promise<{ state?: string; ide?: string }>;
|
||||
};
|
||||
|
||||
export default async function Page(props: Props) {
|
||||
const { state } = await props.searchParams;
|
||||
const { state, ide } = await props.searchParams;
|
||||
|
||||
if (!state) {
|
||||
redirect(`/sign-in`);
|
||||
}
|
||||
|
||||
const { userId, orgId } = await auth();
|
||||
|
||||
const code = userId
|
||||
? await getSignInToken(userId).catch(() => undefined)
|
||||
: undefined;
|
||||
|
||||
if (!code) {
|
||||
redirect(`/sign-in?state=${state}`);
|
||||
redirect(`/sign-in?state=${state}&ide=${ide}`);
|
||||
}
|
||||
|
||||
if (!orgId) {
|
||||
redirect(`/select-org/${state}`);
|
||||
redirect(`/select-org/${state}?ide=${ide}`);
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams({ state, code });
|
||||
const path = `/auth/clerk/callback?${searchParams.toString()}`;
|
||||
const vsCodeUrl = new URL(path, Env.VSCODE_EXTENSION_BASE_URL);
|
||||
const cursorUrl = new URL(path, Env.CURSOR_EXTENSION_BASE_URL);
|
||||
let editor;
|
||||
let deepLinkUrl;
|
||||
|
||||
return <DeepLink vsCodeUrl={vsCodeUrl.href} cursorUrl={cursorUrl.href} />;
|
||||
try {
|
||||
const params = new URLSearchParams({ state, code });
|
||||
|
||||
deepLinkUrl = new URL(
|
||||
`/auth/clerk/callback?${params.toString()}`,
|
||||
ide ?? EXTENSION_URL,
|
||||
).toString();
|
||||
|
||||
editor = new URL(deepLinkUrl).protocol.slice(0, -1);
|
||||
} catch (_) {
|
||||
// Use the defaults if we can't parse the URL.
|
||||
editor = EXTENSION_EDITOR;
|
||||
deepLinkUrl = EXTENSION_URL;
|
||||
}
|
||||
|
||||
return <DeepLink editor={editor} deepLinkUrl={deepLinkUrl} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import { useSessionStorage, useMount } from 'react-use';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { SignIn } from '@clerk/nextjs';
|
||||
|
||||
import { useSetAuthState } from '@/hooks/useAuthState';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const [, setState] = useSessionStorage<string | undefined>('state');
|
||||
|
||||
useMount(() => {
|
||||
const state = searchParams.get('state');
|
||||
|
||||
if (state) {
|
||||
setState(state);
|
||||
}
|
||||
});
|
||||
useSetAuthState();
|
||||
|
||||
return <SignIn />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,20 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import { useSessionStorage, useMount } from 'react-use';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { SignUp } from '@clerk/nextjs';
|
||||
|
||||
import { useSetAuthState } from '@/hooks/useAuthState';
|
||||
|
||||
export default function Page() {
|
||||
const searchParams = useSearchParams();
|
||||
const [, setState] = useSessionStorage<string | undefined>('state');
|
||||
|
||||
useMount(() => {
|
||||
const state = searchParams.get('state');
|
||||
|
||||
if (state) {
|
||||
setState(state);
|
||||
}
|
||||
});
|
||||
useSetAuthState();
|
||||
|
||||
return <SignUp />;
|
||||
}
|
||||
|
|
|
|||
43
src/hooks/useAuthState.ts
Normal file
43
src/hooks/useAuthState.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
'use client';
|
||||
|
||||
import { useCallback } from 'react';
|
||||
import { useSessionStorage, useMount } from 'react-use';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
|
||||
import { EXTENSION_URL } from '@/lib/constants';
|
||||
|
||||
export type AuthState = {
|
||||
state?: string;
|
||||
ide?: string;
|
||||
};
|
||||
|
||||
export type SetAuthState = (state: AuthState) => void;
|
||||
|
||||
export const useAuthState = (): AuthState & { set: SetAuthState } => {
|
||||
const [state, setState] = useSessionStorage<string | undefined>('state');
|
||||
const [ide, setIde] = useSessionStorage<string | undefined>('ide');
|
||||
|
||||
const set = useCallback(
|
||||
(state: AuthState) => {
|
||||
setState(state.state);
|
||||
setIde(state.ide);
|
||||
},
|
||||
[setState, setIde],
|
||||
);
|
||||
|
||||
return { state, ide, set };
|
||||
};
|
||||
|
||||
export const useSetAuthState = () => {
|
||||
const { set } = useAuthState();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
useMount(() => {
|
||||
const state = searchParams.get('state');
|
||||
const ide = searchParams.get('ide');
|
||||
|
||||
if (state) {
|
||||
set({ state, ide: ide ?? EXTENSION_URL });
|
||||
}
|
||||
});
|
||||
};
|
||||
9
src/lib/constants.ts
Normal file
9
src/lib/constants.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export const EXTENSION_EDITOR = 'vscode';
|
||||
|
||||
export const EXTENSION_PUBLISHER = 'RooVeterinaryInc';
|
||||
|
||||
export const EXTENSION_NAME = 'roo-cline';
|
||||
|
||||
export const EXTENSION_IDENTIFIER = `${EXTENSION_PUBLISHER}.${EXTENSION_NAME}`;
|
||||
|
||||
export const EXTENSION_URL = `vscode://${EXTENSION_IDENTIFIER}`;
|
||||
|
|
@ -6,8 +6,6 @@ export const Env = createEnv({
|
|||
CLERK_SECRET_KEY: z.string().min(1),
|
||||
DATABASE_URL: z.string(),
|
||||
LOGTAIL_SOURCE_TOKEN: z.string().optional(),
|
||||
VSCODE_EXTENSION_BASE_URL: z.string().min(1),
|
||||
CURSOR_EXTENSION_BASE_URL: z.string().min(1),
|
||||
CLICKHOUSE_URL: z.string().min(1),
|
||||
CLICKHOUSE_PASSWORD: z.string().min(1),
|
||||
},
|
||||
|
|
@ -29,8 +27,6 @@ export const Env = createEnv({
|
|||
CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY,
|
||||
DATABASE_URL: process.env.DATABASE_URL,
|
||||
LOGTAIL_SOURCE_TOKEN: process.env.LOGTAIL_SOURCE_TOKEN,
|
||||
VSCODE_EXTENSION_BASE_URL: process.env.VSCODE_EXTENSION_BASE_URL,
|
||||
CURSOR_EXTENSION_BASE_URL: process.env.CURSOR_EXTENSION_BASE_URL,
|
||||
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
|
||||
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY:
|
||||
process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue