From 0c1b5c050be84ff6d91f4dd770d19b3b9356e4df Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Sun, 24 Nov 2024 20:11:08 -0500 Subject: [PATCH 01/36] GitHub authentication to the application using GitHub's Device Flow OAuth process. --- app/components/sidebar/Menu.client.tsx | 28 ++++- app/github/GitHubAuth.tsx | 157 +++++++++++++++++++++++++ app/github/config.ts | 10 ++ app/github/github.client.ts | 24 ++++ app/github/useGitHubAuth.ts | 54 +++++++++ app/routes/api.github.proxy.ts | 60 ++++++++++ 6 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 app/github/GitHubAuth.tsx create mode 100644 app/github/config.ts create mode 100644 app/github/github.client.ts create mode 100644 app/github/useGitHubAuth.ts create mode 100644 app/routes/api.github.proxy.ts diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx index 5becf82c4..0324faefa 100644 --- a/app/components/sidebar/Menu.client.tsx +++ b/app/components/sidebar/Menu.client.tsx @@ -8,6 +8,8 @@ import { cubicEasingFn } from '~/utils/easings'; import { logger } from '~/utils/logger'; import { HistoryItem } from './HistoryItem'; import { binDates } from './date-binning'; +import { GitHubAuth } from '~/github/GitHubAuth'; +import { useGitHubAuth } from '~/github/useGitHubAuth'; const menuVariants = { closed: { @@ -38,6 +40,7 @@ export function Menu() { const [list, setList] = useState([]); const [open, setOpen] = useState(false); const [dialogContent, setDialogContent] = useState(null); + const { isAuthenticated, handleAuthComplete, handleLogout } = useGitHubAuth(); const loadEntries = useCallback(() => { if (db) { @@ -122,11 +125,34 @@ export function Menu() {
Start new chat + {isAuthenticated ? ( + + ) : ( + toast.error(error.message)} + > + + + )}
Your Chats
diff --git a/app/github/GitHubAuth.tsx b/app/github/GitHubAuth.tsx new file mode 100644 index 000000000..b07c94716 --- /dev/null +++ b/app/github/GitHubAuth.tsx @@ -0,0 +1,157 @@ +import React, { useState, useCallback, useEffect } from 'react'; +import { GITHUB_CONFIG } from './config'; + +interface GitHubAuthProps { + onAuthComplete?: (token: string) => void; + onError?: (error: Error) => void; + children?: React.ReactNode; +} + +interface DeviceCodeResponse { + device_code: string; + user_code: string; + verification_uri: string; + expires_in: number; + interval: number; +} + +interface AccessTokenResponse { + access_token?: string; + error?: string; + error_description?: string; +} + +export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProps) { + const [isLoading, setIsLoading] = useState(false); + const [deviceCode, setDeviceCode] = useState(null); + const [userCode, setUserCode] = useState(null); + const [verificationUrl, setVerificationUrl] = useState(null); + const [isPolling, setIsPolling] = useState(false); + + const pollForToken = useCallback(async (code: string, interval: number) => { + try { + const params = new URLSearchParams({ + endpoint: GITHUB_CONFIG.accessTokenEndpoint, + client_id: GITHUB_CONFIG.clientId, + device_code: code, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code' + }); + + const response = await fetch(`${GITHUB_CONFIG.proxyUrl}?${params}`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + } + }); + + const data: AccessTokenResponse = await response.json(); + + if (data.access_token) { + localStorage.setItem('github_token', data.access_token); + setIsPolling(false); + setIsLoading(false); + onAuthComplete?.(data.access_token); + } else if (data.error === 'authorization_pending') { + // Continue polling + setTimeout(() => pollForToken(code, interval), interval * 1000); + } else { + throw new Error(data.error_description || 'Authentication failed'); + } + } catch (error: any) { + setIsPolling(false); + setIsLoading(false); + onError?.(error); + } + }, [onAuthComplete, onError]); + + const startAuth = useCallback(async () => { + try { + setIsLoading(true); + + const params = new URLSearchParams({ + endpoint: GITHUB_CONFIG.deviceCodeEndpoint, + client_id: GITHUB_CONFIG.clientId, + scope: GITHUB_CONFIG.scope, + }); + + const response = await fetch(`${GITHUB_CONFIG.proxyUrl}?${params}`, { + method: 'POST', + headers: { + 'Accept': 'application/json', + } + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => null); + throw new Error( + errorData?.error || + `Failed to start authentication process (${response.status})` + ); + } + + const data: DeviceCodeResponse = await response.json(); + + if (!data.device_code || !data.user_code || !data.verification_uri) { + throw new Error('Invalid response from GitHub'); + } + + setDeviceCode(data.device_code); + setUserCode(data.user_code); + setVerificationUrl(data.verification_uri); + setIsPolling(true); + + pollForToken(data.device_code, data.interval || 5); + } catch (error: any) { + setIsLoading(false); + onError?.(error); + } + }, [pollForToken, onError]); + + useEffect(() => { + return () => { + setIsPolling(false); + }; + }, []); + + if (userCode && verificationUrl) { + return ( +
+

Enter this code on GitHub:

+
+ {userCode} +
+ + Click here to open GitHub + + {isPolling && ( +

+ Waiting for authentication... You can close the GitHub window once authorized. +

+ )} +
+ ); + } + + if (children) { + return ( +
+ {children} +
+ ); + } + + return ( + + ); +} diff --git a/app/github/config.ts b/app/github/config.ts new file mode 100644 index 000000000..976c3247a --- /dev/null +++ b/app/github/config.ts @@ -0,0 +1,10 @@ +export const GITHUB_CONFIG = { + clientId: import.meta.env.VITE_GITHUB_CLIENT_ID || '', + scope: 'read:user', + proxyUrl: '/api/github/proxy', + deviceCodeEndpoint: '/login/device/code', + accessTokenEndpoint: '/login/oauth/access_token', + userApiUrl: 'https://api.github.com/user', + pollInterval: 5, + maxPollAttempts: 12, +} as const; diff --git a/app/github/github.client.ts b/app/github/github.client.ts new file mode 100644 index 000000000..1c7940b1d --- /dev/null +++ b/app/github/github.client.ts @@ -0,0 +1,24 @@ +import { GITHUB_CONFIG } from './config'; + +export interface GitHubUser { + login: string; + id: number; + avatar_url: string; + name: string; + email: string; +} + +export async function getGitHubUser(accessToken: string): Promise { + const response = await fetch(GITHUB_CONFIG.userApiUrl, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Accept': 'application/json', + }, + }); + + if (!response.ok) { + throw new Error('Failed to get user info'); + } + + return response.json(); +} diff --git a/app/github/useGitHubAuth.ts b/app/github/useGitHubAuth.ts new file mode 100644 index 000000000..1d14c8c33 --- /dev/null +++ b/app/github/useGitHubAuth.ts @@ -0,0 +1,54 @@ +import { useCallback, useEffect, useState } from 'react'; +import { getGitHubUser, type GitHubUser } from './github.client'; + +export function useGitHubAuth() { + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [user, setUser] = useState(null); + + useEffect(() => { + const checkAuth = async () => { + const token = localStorage.getItem('github_token'); + if (token) { + try { + const userInfo = await getGitHubUser(token); + setUser(userInfo); + setIsAuthenticated(true); + } catch (error) { + // Token might be invalid, remove it + localStorage.removeItem('github_token'); + setIsAuthenticated(false); + } + } else { + setIsAuthenticated(false); + } + setIsLoading(false); + }; + + checkAuth(); + }, []); + + const handleAuthComplete = useCallback(async (token: string) => { + try { + const userInfo = await getGitHubUser(token); + setUser(userInfo); + setIsAuthenticated(true); + } catch (error) { + console.error('Failed to get user info:', error); + } + }, []); + + const handleLogout = useCallback(() => { + localStorage.removeItem('github_token'); + setUser(null); + setIsAuthenticated(false); + }, []); + + return { + isAuthenticated, + isLoading, + user, + handleAuthComplete, + handleLogout + }; +} diff --git a/app/routes/api.github.proxy.ts b/app/routes/api.github.proxy.ts new file mode 100644 index 000000000..df2016103 --- /dev/null +++ b/app/routes/api.github.proxy.ts @@ -0,0 +1,60 @@ +import type { ActionFunctionArgs } from '@remix-run/cloudflare'; + +export async function action({ request }: ActionFunctionArgs) { + const url = new URL(request.url); + const targetEndpoint = url.searchParams.get('endpoint'); + const clientId = url.searchParams.get('client_id'); + const deviceCode = url.searchParams.get('device_code'); + const grantType = url.searchParams.get('grant_type'); + const scope = url.searchParams.get('scope'); + + if (!targetEndpoint || !clientId) { + return new Response('Missing required parameters', { status: 400 }); + } + + const githubUrl = `https://github.com${targetEndpoint}`; + const body: Record = { client_id: clientId }; + + if (deviceCode) body.device_code = deviceCode; + if (grantType) body.grant_type = grantType; + if (scope) body.scope = scope; + + try { + const response = await fetch(githubUrl, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + + const data = await response.json(); + + return new Response(JSON.stringify(data), { + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }); + } catch (error) { + return new Response(JSON.stringify({ error: 'Failed to proxy request' }), { + status: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }); + } +} + +// Handle preflight requests +export async function options() { + return new Response(null, { + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }); +} From 60bbee49c10bd68a8979be3abab9da75ad20eba8 Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Sun, 24 Nov 2024 20:47:47 -0500 Subject: [PATCH 02/36] Add type for error response --- app/github/GitHubAuth.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/github/GitHubAuth.tsx b/app/github/GitHubAuth.tsx index b07c94716..59992f8d9 100644 --- a/app/github/GitHubAuth.tsx +++ b/app/github/GitHubAuth.tsx @@ -7,6 +7,11 @@ interface GitHubAuthProps { children?: React.ReactNode; } +interface GitHubErrorResponse { + error?: string; + error_description?: string; +} + interface DeviceCodeResponse { device_code: string; user_code: string; @@ -82,7 +87,7 @@ export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProp }); if (!response.ok) { - const errorData = await response.json().catch(() => null); + const errorData = await response.json().catch(() => ({} as GitHubErrorResponse)); throw new Error( errorData?.error || `Failed to start authentication process (${response.status})` From 14c289f8a712912cf5428d14998cb016e428605c Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Sun, 24 Nov 2024 20:53:20 -0500 Subject: [PATCH 03/36] fix error handling --- app/github/GitHubAuth.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/github/GitHubAuth.tsx b/app/github/GitHubAuth.tsx index 59992f8d9..526c58d05 100644 --- a/app/github/GitHubAuth.tsx +++ b/app/github/GitHubAuth.tsx @@ -87,11 +87,16 @@ export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProp }); if (!response.ok) { - const errorData = await response.json().catch(() => ({} as GitHubErrorResponse)); - throw new Error( - errorData?.error || - `Failed to start authentication process (${response.status})` - ); + let errorMessage = `Failed to start authentication process (${response.status})`; + try { + const errorData: GitHubErrorResponse = await response.json(); + if (errorData.error) { + errorMessage = errorData.error_description || errorData.error; + } + } catch { + // Use default error message if JSON parsing fails + } + throw new Error(errorMessage); } const data: DeviceCodeResponse = await response.json(); From e43953e2dc4cdb1095cf2971c50d1c139eae2e2f Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Sun, 24 Nov 2024 21:04:43 -0500 Subject: [PATCH 04/36] moved /github to /lib --- app/components/sidebar/Menu.client.tsx | 4 ++-- app/{ => lib}/github/GitHubAuth.tsx | 0 app/{ => lib}/github/config.ts | 0 app/{ => lib}/github/github.client.ts | 0 app/{ => lib}/github/useGitHubAuth.ts | 0 5 files changed, 2 insertions(+), 2 deletions(-) rename app/{ => lib}/github/GitHubAuth.tsx (100%) rename app/{ => lib}/github/config.ts (100%) rename app/{ => lib}/github/github.client.ts (100%) rename app/{ => lib}/github/useGitHubAuth.ts (100%) diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx index 0324faefa..50f69aa5f 100644 --- a/app/components/sidebar/Menu.client.tsx +++ b/app/components/sidebar/Menu.client.tsx @@ -8,8 +8,8 @@ import { cubicEasingFn } from '~/utils/easings'; import { logger } from '~/utils/logger'; import { HistoryItem } from './HistoryItem'; import { binDates } from './date-binning'; -import { GitHubAuth } from '~/github/GitHubAuth'; -import { useGitHubAuth } from '~/github/useGitHubAuth'; +import { GitHubAuth } from '~/lib/github/GitHubAuth'; +import { useGitHubAuth } from '~/lib/github/useGitHubAuth'; const menuVariants = { closed: { diff --git a/app/github/GitHubAuth.tsx b/app/lib/github/GitHubAuth.tsx similarity index 100% rename from app/github/GitHubAuth.tsx rename to app/lib/github/GitHubAuth.tsx diff --git a/app/github/config.ts b/app/lib/github/config.ts similarity index 100% rename from app/github/config.ts rename to app/lib/github/config.ts diff --git a/app/github/github.client.ts b/app/lib/github/github.client.ts similarity index 100% rename from app/github/github.client.ts rename to app/lib/github/github.client.ts diff --git a/app/github/useGitHubAuth.ts b/app/lib/github/useGitHubAuth.ts similarity index 100% rename from app/github/useGitHubAuth.ts rename to app/lib/github/useGitHubAuth.ts From e4af820caed90cf25fbbdb7a091bfa32650d24c9 Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Sat, 14 Dec 2024 11:50:08 -0500 Subject: [PATCH 05/36] merge main --- app/commit.json | 2 +- app/components/chat/BaseChat.tsx | 6 +- app/components/sidebar/Menu.client.tsx | 24 +++++- app/lib/github/GitHubAuth.tsx | 108 +++++++++++++++---------- app/lib/github/config.ts | 2 +- app/lib/github/github.client.ts | 4 +- app/lib/github/useGitHubAuth.ts | 10 ++- app/lib/stores/workbench.ts | 1 - app/routes/api.github.proxy.ts | 18 +++-- 9 files changed, 117 insertions(+), 58 deletions(-) diff --git a/app/commit.json b/app/commit.json index aab1ae008..d5c5647c1 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "0822ccd1b315eb6d525b998911d561b45f903362" } +{ "commit": "1b61aabc6bd8cabe826089c24d97143a7aff8ef2" } diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index 0ed6a30b6..adbc06cab 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -425,15 +425,17 @@ export const BaseChat = React.forwardRef( } event.preventDefault(); - + if (isStreaming) { handleStop?.(); return; } + // ignore if using input method engine if (event.nativeEvent.isComposing) { - return + return; } + handleSendMessage?.(event); } }} diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx index 82c6f1599..2d2ee1ade 100644 --- a/app/components/sidebar/Menu.client.tsx +++ b/app/components/sidebar/Menu.client.tsx @@ -154,11 +154,33 @@ export const Menu = () => {
Start new chat + {isAuthenticated ? ( + + ) : ( + toast.error(error.message)}> + + + )} +
+
(null); + const [, setDeviceCode] = useState(null); const [userCode, setUserCode] = useState(null); const [verificationUrl, setVerificationUrl] = useState(null); const [isPolling, setIsPolling] = useState(false); - - const pollForToken = useCallback(async (code: string, interval: number) => { - try { - const params = new URLSearchParams({ - endpoint: GITHUB_CONFIG.accessTokenEndpoint, - client_id: GITHUB_CONFIG.clientId, - device_code: code, - grant_type: 'urn:ietf:params:oauth:grant-type:device_code' - }); - - const response = await fetch(`${GITHUB_CONFIG.proxyUrl}?${params}`, { - method: 'POST', - headers: { - 'Accept': 'application/json', + const [showCopied, setShowCopied] = useState(false); + + const pollForToken = useCallback( + async (code: string, interval: number) => { + try { + const params = new URLSearchParams({ + endpoint: GITHUB_CONFIG.accessTokenEndpoint, + client_id: GITHUB_CONFIG.clientId, + device_code: code, + grant_type: 'urn:ietf:params:oauth:grant-type:device_code', + }); + + const response = await fetch(`${GITHUB_CONFIG.proxyUrl}?${params}`, { + method: 'POST', + headers: { + Accept: 'application/json', + }, + }); + + const data: AccessTokenResponse = await response.json(); + + if (data.access_token) { + localStorage.setItem('github_token', data.access_token); + setIsPolling(false); + setIsLoading(false); + onAuthComplete?.(data.access_token); + } else if (data.error === 'authorization_pending') { + // Continue polling + setTimeout(() => pollForToken(code, interval), interval * 1000); + } else { + throw new Error(data.error_description || 'Authentication failed'); } - }); - - const data: AccessTokenResponse = await response.json(); - - if (data.access_token) { - localStorage.setItem('github_token', data.access_token); + } catch (error: any) { setIsPolling(false); setIsLoading(false); - onAuthComplete?.(data.access_token); - } else if (data.error === 'authorization_pending') { - // Continue polling - setTimeout(() => pollForToken(code, interval), interval * 1000); - } else { - throw new Error(data.error_description || 'Authentication failed'); + onError?.(error); } - } catch (error: any) { - setIsPolling(false); - setIsLoading(false); - onError?.(error); - } - }, [onAuthComplete, onError]); + }, + [onAuthComplete, onError], + ); const startAuth = useCallback(async () => { try { setIsLoading(true); - + const params = new URLSearchParams({ endpoint: GITHUB_CONFIG.deviceCodeEndpoint, client_id: GITHUB_CONFIG.clientId, @@ -82,14 +86,16 @@ export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProp const response = await fetch(`${GITHUB_CONFIG.proxyUrl}?${params}`, { method: 'POST', headers: { - 'Accept': 'application/json', - } + Accept: 'application/json', + }, }); if (!response.ok) { let errorMessage = `Failed to start authentication process (${response.status})`; + try { const errorData: GitHubErrorResponse = await response.json(); + if (errorData.error) { errorMessage = errorData.error_description || errorData.error; } @@ -100,7 +106,7 @@ export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProp } const data: DeviceCodeResponse = await response.json(); - + if (!data.device_code || !data.user_code || !data.verification_uri) { throw new Error('Invalid response from GitHub'); } @@ -109,7 +115,7 @@ export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProp setUserCode(data.user_code); setVerificationUrl(data.verification_uri); setIsPolling(true); - + pollForToken(data.device_code, data.interval || 5); } catch (error: any) { setIsLoading(false); @@ -117,6 +123,14 @@ export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProp } }, [pollForToken, onError]); + const handleCopy = useCallback(() => { + if (userCode) { + navigator.clipboard.writeText(userCode); + setShowCopied(true); + setTimeout(() => setShowCopied(false), 2000); + } + }, [userCode]); + useEffect(() => { return () => { setIsPolling(false); @@ -127,14 +141,26 @@ export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProp return (

Enter this code on GitHub:

-
- {userCode} +
+
{ + handleCopy(); + + const target = e.currentTarget; + target.classList.add('bg-green-100'); + setTimeout(() => target.classList.remove('bg-green-100'), 200); + }} + > + {userCode} +
+ {showCopied &&
Copied!
}
Click here to open GitHub diff --git a/app/lib/github/config.ts b/app/lib/github/config.ts index 976c3247a..b43dbc9bf 100644 --- a/app/lib/github/config.ts +++ b/app/lib/github/config.ts @@ -6,5 +6,5 @@ export const GITHUB_CONFIG = { accessTokenEndpoint: '/login/oauth/access_token', userApiUrl: 'https://api.github.com/user', pollInterval: 5, - maxPollAttempts: 12, + maxPollAttempts: 12, } as const; diff --git a/app/lib/github/github.client.ts b/app/lib/github/github.client.ts index 1c7940b1d..e183313b9 100644 --- a/app/lib/github/github.client.ts +++ b/app/lib/github/github.client.ts @@ -11,8 +11,8 @@ export interface GitHubUser { export async function getGitHubUser(accessToken: string): Promise { const response = await fetch(GITHUB_CONFIG.userApiUrl, { headers: { - 'Authorization': `Bearer ${accessToken}`, - 'Accept': 'application/json', + Authorization: `Bearer ${accessToken}`, + Accept: 'application/json', }, }); diff --git a/app/lib/github/useGitHubAuth.ts b/app/lib/github/useGitHubAuth.ts index 1d14c8c33..73217ccb7 100644 --- a/app/lib/github/useGitHubAuth.ts +++ b/app/lib/github/useGitHubAuth.ts @@ -9,12 +9,13 @@ export function useGitHubAuth() { useEffect(() => { const checkAuth = async () => { const token = localStorage.getItem('github_token'); + if (token) { try { const userInfo = await getGitHubUser(token); setUser(userInfo); setIsAuthenticated(true); - } catch (error) { + } catch { // Token might be invalid, remove it localStorage.removeItem('github_token'); setIsAuthenticated(false); @@ -22,6 +23,7 @@ export function useGitHubAuth() { } else { setIsAuthenticated(false); } + setIsLoading(false); }; @@ -33,8 +35,8 @@ export function useGitHubAuth() { const userInfo = await getGitHubUser(token); setUser(userInfo); setIsAuthenticated(true); - } catch (error) { - console.error('Failed to get user info:', error); + } catch (_error) { + console.error('Failed to get user info:', _error); } }, []); @@ -49,6 +51,6 @@ export function useGitHubAuth() { isLoading, user, handleAuthComplete, - handleLogout + handleLogout, }; } diff --git a/app/lib/stores/workbench.ts b/app/lib/stores/workbench.ts index bbd537d40..0d46057db 100644 --- a/app/lib/stores/workbench.ts +++ b/app/lib/stores/workbench.ts @@ -297,7 +297,6 @@ export class WorkbenchStore { const action = artifact.runner.actions.get()[data.actionId]; - if (!action || action.executed) { return; } diff --git a/app/routes/api.github.proxy.ts b/app/routes/api.github.proxy.ts index df2016103..813111f1b 100644 --- a/app/routes/api.github.proxy.ts +++ b/app/routes/api.github.proxy.ts @@ -15,15 +15,23 @@ export async function action({ request }: ActionFunctionArgs) { const githubUrl = `https://github.com${targetEndpoint}`; const body: Record = { client_id: clientId }; - if (deviceCode) body.device_code = deviceCode; - if (grantType) body.grant_type = grantType; - if (scope) body.scope = scope; + if (deviceCode) { + body.device_code = deviceCode; + } + + if (grantType) { + body.grant_type = grantType; + } + + if (scope) { + body.scope = scope; + } try { const response = await fetch(githubUrl, { method: 'POST', headers: { - 'Accept': 'application/json', + Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify(body), @@ -37,7 +45,7 @@ export async function action({ request }: ActionFunctionArgs) { 'Access-Control-Allow-Origin': '*', }, }); - } catch (error) { + } catch { return new Response(JSON.stringify({ error: 'Failed to proxy request' }), { status: 500, headers: { From 0e2c9fb3d7214da08dd30a491cab7e239f21a7cf Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Sat, 14 Dec 2024 14:45:29 -0500 Subject: [PATCH 06/36] GitHub OAuth optional feature --- app/commit.json | 2 +- .../settings/features/FeaturesTab.tsx | 9 +- app/components/sidebar/Menu.client.tsx | 27 ++- app/lib/github/GitHubAuth.tsx | 154 +++++++++--------- app/lib/github/useGitHubAuth.ts | 101 +++++++++--- app/lib/hooks/useSettings.tsx | 25 +++ app/lib/stores/settings.ts | 2 + app/routes/api.github.proxy.ts | 16 +- 8 files changed, 228 insertions(+), 108 deletions(-) diff --git a/app/commit.json b/app/commit.json index d5c5647c1..349125ad6 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "1b61aabc6bd8cabe826089c24d97143a7aff8ef2" } +{ "commit": "e4af820caed90cf25fbbdb7a091bfa32650d24c9" } diff --git a/app/components/settings/features/FeaturesTab.tsx b/app/components/settings/features/FeaturesTab.tsx index 9c9e4a00e..79e588e9b 100644 --- a/app/components/settings/features/FeaturesTab.tsx +++ b/app/components/settings/features/FeaturesTab.tsx @@ -3,7 +3,7 @@ import { Switch } from '~/components/ui/Switch'; import { useSettings } from '~/lib/hooks/useSettings'; export default function FeaturesTab() { - const { debug, enableDebugMode, isLocalModel, enableLocalModels, eventLogs, enableEventLogs } = useSettings(); + const { debug, enableDebugMode, isLocalModel, enableLocalModels, eventLogs, enableEventLogs, isGitHubAuth, enableGitHubAuth } = useSettings(); return (
@@ -16,6 +16,13 @@ export default function FeaturesTab() { Event Logs
+
+
+ GitHub Auth + +
+

A utility feature that Provides GitHub authentication. If your feature needs GitHub authentication you can use this. The useGitHubAuth() hook provides authentication state including login status, loading state, and user information. Once authenticated, you can access the GitHub token from localStorage.

+
diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx index 2d2ee1ade..e054cda3d 100644 --- a/app/components/sidebar/Menu.client.tsx +++ b/app/components/sidebar/Menu.client.tsx @@ -65,6 +65,15 @@ export const Menu = () => { const { isAuthenticated, handleAuthComplete, handleLogout } = useGitHubAuth(); const [isSettingsOpen, setIsSettingsOpen] = useState(false); + const handleGitHubAuthComplete = useCallback((token: string) => { + handleAuthComplete(token); + }, [handleAuthComplete]); + + const handleGitHubError = useCallback((error: Error) => { + toast.error(error.message); + localStorage.removeItem('github_token'); + }, []); + const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({ items: list, searchFields: ['description'], @@ -170,14 +179,16 @@ export const Menu = () => { Disconnect GitHub ) : ( - toast.error(error.message)}> - - +
+ + + +
)}
diff --git a/app/lib/github/GitHubAuth.tsx b/app/lib/github/GitHubAuth.tsx index 69c3c3be2..ef37820dd 100644 --- a/app/lib/github/GitHubAuth.tsx +++ b/app/lib/github/GitHubAuth.tsx @@ -1,5 +1,6 @@ import React, { useState, useCallback, useEffect } from 'react'; import { GITHUB_CONFIG } from './config'; +import { useSettings } from '~/lib/hooks/useSettings'; interface GitHubAuthProps { onAuthComplete?: (token: string) => void; @@ -28,14 +29,21 @@ interface AccessTokenResponse { export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProps) { const [isLoading, setIsLoading] = useState(false); - const [, setDeviceCode] = useState(null); + const [deviceCode, setDeviceCode] = useState(null); const [userCode, setUserCode] = useState(null); const [verificationUrl, setVerificationUrl] = useState(null); const [isPolling, setIsPolling] = useState(false); const [showCopied, setShowCopied] = useState(false); + const { isGitHubAuth } = useSettings(); const pollForToken = useCallback( - async (code: string, interval: number) => { + async (code: string, interval: number, attempts = 0) => { + if (attempts >= GITHUB_CONFIG.maxPollAttempts) { + setIsPolling(false); + onError?.(new Error('Authentication timed out. Please try again.')); + return; + } + try { const params = new URLSearchParams({ endpoint: GITHUB_CONFIG.accessTokenEndpoint, @@ -51,29 +59,35 @@ export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProp }, }); - const data: AccessTokenResponse = await response.json(); - - if (data.access_token) { - localStorage.setItem('github_token', data.access_token); - setIsPolling(false); - setIsLoading(false); - onAuthComplete?.(data.access_token); - } else if (data.error === 'authorization_pending') { - // Continue polling - setTimeout(() => pollForToken(code, interval), interval * 1000); - } else { - throw new Error(data.error_description || 'Authentication failed'); + const data = await response.json(); + + if (response.status === 202) { + // Authorization is pending, continue polling + setTimeout(() => pollForToken(code, interval, attempts + 1), interval * 1000); + return; + } + + if (!response.ok) { + throw new Error(data.error_description || data.error || 'Authentication failed'); } + + if (!data.access_token) { + throw new Error('Invalid response from GitHub'); + } + + // Store the token in localStorage before completing auth + localStorage.setItem('github_token', data.access_token); + setIsPolling(false); + onAuthComplete?.(data.access_token); } catch (error: any) { setIsPolling(false); - setIsLoading(false); onError?.(error); } }, [onAuthComplete, onError], ); - const startAuth = useCallback(async () => { + const initializeAuth = useCallback(async () => { try { setIsLoading(true); @@ -91,18 +105,8 @@ export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProp }); if (!response.ok) { - let errorMessage = `Failed to start authentication process (${response.status})`; - - try { - const errorData: GitHubErrorResponse = await response.json(); - - if (errorData.error) { - errorMessage = errorData.error_description || errorData.error; - } - } catch { - // Use default error message if JSON parsing fails - } - throw new Error(errorMessage); + const data = await response.json(); + throw new Error(data.error_description || data.error || 'Failed to start authentication process'); } const data: DeviceCodeResponse = await response.json(); @@ -116,14 +120,23 @@ export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProp setVerificationUrl(data.verification_uri); setIsPolling(true); - pollForToken(data.device_code, data.interval || 5); + pollForToken(data.device_code, data.interval || GITHUB_CONFIG.pollInterval); } catch (error: any) { setIsLoading(false); onError?.(error); + } finally { + setIsLoading(false); } }, [pollForToken, onError]); - const handleCopy = useCallback(() => { + useEffect(() => { + return () => { + setIsPolling(false); + setIsLoading(false); + }; + }, []); + + const handleCopyCode = useCallback(() => { if (userCode) { navigator.clipboard.writeText(userCode); setShowCopied(true); @@ -131,63 +144,52 @@ export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProp } }, [userCode]); - useEffect(() => { - return () => { - setIsPolling(false); - }; - }, []); + if (!isGitHubAuth) { + return null; + } + + if (isLoading) { + return ( +
+
+

Initializing GitHub authentication...

+
+ ); + } if (userCode && verificationUrl) { return ( -
-

Enter this code on GitHub:

-
-
{ - handleCopy(); - - const target = e.currentTarget; - target.classList.add('bg-green-100'); - setTimeout(() => target.classList.remove('bg-green-100'), 200); - }} - > +
+

Enter this code at {verificationUrl}

+
+ {userCode} -
- {showCopied &&
Copied!
} +
+
- - Click here to open GitHub - {isPolling && (

- Waiting for authentication... You can close the GitHub window once authorized. + Waiting for authorization... You can close the GitHub window once authorized.

)}
); } - if (children) { - return ( -
- {children} -
- ); - } - - return ( - - ); + return React.cloneElement(children as React.ReactElement, { + onClick: (e: React.MouseEvent) => { + e.preventDefault(); + initializeAuth(); + (children as React.ReactElement).props.onClick?.(e); + }, + }); } diff --git a/app/lib/github/useGitHubAuth.ts b/app/lib/github/useGitHubAuth.ts index 73217ccb7..2833bdbca 100644 --- a/app/lib/github/useGitHubAuth.ts +++ b/app/lib/github/useGitHubAuth.ts @@ -1,42 +1,101 @@ import { useCallback, useEffect, useState } from 'react'; import { getGitHubUser, type GitHubUser } from './github.client'; +import { isGitHubAuthEnabled } from '~/lib/stores/settings'; +import { useStore } from '@nanostores/react'; + +// Create a global variable to cache the auth state +let cachedUser: GitHubUser | null = null; +let cachedIsAuthenticated = false; export function useGitHubAuth() { - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [user, setUser] = useState(null); + const [isAuthenticated, setIsAuthenticated] = useState(cachedIsAuthenticated); + const [isLoading, setIsLoading] = useState(!cachedUser); + const [user, setUser] = useState(cachedUser); + const isGitHubAuth = useStore(isGitHubAuthEnabled); - useEffect(() => { - const checkAuth = async () => { - const token = localStorage.getItem('github_token'); - - if (token) { - try { - const userInfo = await getGitHubUser(token); - setUser(userInfo); - setIsAuthenticated(true); - } catch { - // Token might be invalid, remove it + const checkAuth = useCallback(async () => { + // If GitHub auth is disabled, don't authenticate + if (!isGitHubAuth) { + if (isAuthenticated) { // Only clear if we were previously authenticated + setIsAuthenticated(false); + setUser(null); + cachedUser = null; + cachedIsAuthenticated = false; + localStorage.removeItem('github_token'); + } + setIsLoading(false); + return; + } + + const token = localStorage.getItem('github_token'); + + if (token) { + try { + const userInfo = await getGitHubUser(token); + setUser(userInfo); + setIsAuthenticated(true); + cachedUser = userInfo; + cachedIsAuthenticated = true; + } catch (error) { + // Only remove token if it's an auth error (401 or 403) + if (error instanceof Error && 'status' in error && (error.status === 401 || error.status === 403)) { localStorage.removeItem('github_token'); - setIsAuthenticated(false); } - } else { setIsAuthenticated(false); + setUser(null); + cachedUser = null; + cachedIsAuthenticated = false; } + } else { + setIsAuthenticated(false); + setUser(null); + cachedUser = null; + cachedIsAuthenticated = false; + } + setIsLoading(false); + }, [isGitHubAuth, isAuthenticated]); - setIsLoading(false); - }; + // Initial auth check + useEffect(() => { + if (!cachedUser || !cachedIsAuthenticated) { + checkAuth(); + } + }, []); // Only run on mount - checkAuth(); - }, []); + // Handle GitHub auth toggle + useEffect(() => { + if (!isGitHubAuth && isAuthenticated) { + // Clear auth when feature is disabled + setIsAuthenticated(false); + setUser(null); + cachedUser = null; + cachedIsAuthenticated = false; + localStorage.removeItem('github_token'); + } else if (isGitHubAuth && !isAuthenticated) { + // Try to authenticate when feature is enabled + checkAuth(); + } + }, [isGitHubAuth, isAuthenticated, checkAuth]); + + // Re-run auth check when window regains focus + useEffect(() => { + window.addEventListener('focus', checkAuth); + return () => { + window.removeEventListener('focus', checkAuth); + }; + }, [checkAuth]); const handleAuthComplete = useCallback(async (token: string) => { try { const userInfo = await getGitHubUser(token); setUser(userInfo); setIsAuthenticated(true); + cachedUser = userInfo; + cachedIsAuthenticated = true; } catch (_error) { console.error('Failed to get user info:', _error); + cachedUser = null; + cachedIsAuthenticated = false; } }, []); @@ -44,6 +103,8 @@ export function useGitHubAuth() { localStorage.removeItem('github_token'); setUser(null); setIsAuthenticated(false); + cachedUser = null; + cachedIsAuthenticated = false; }, []); return { diff --git a/app/lib/hooks/useSettings.tsx b/app/lib/hooks/useSettings.tsx index 0e7965163..8cc7815f5 100644 --- a/app/lib/hooks/useSettings.tsx +++ b/app/lib/hooks/useSettings.tsx @@ -3,6 +3,7 @@ import { isDebugMode, isEventLogsEnabled, isLocalModelsEnabled, + isGitHubAuthEnabled, LOCAL_PROVIDERS, providersStore, } from '~/lib/stores/settings'; @@ -16,6 +17,7 @@ export function useSettings() { const debug = useStore(isDebugMode); const eventLogs = useStore(isEventLogsEnabled); const isLocalModel = useStore(isLocalModelsEnabled); + const isGitHubAuth = useStore(isGitHubAuthEnabled); const [activeProviders, setActiveProviders] = useState([]); // reading values from cookies on mount @@ -60,6 +62,13 @@ export function useSettings() { if (savedLocalModels) { isLocalModelsEnabled.set(savedLocalModels === 'true'); } + + // load GitHub authentication from cookies + const savedGitHubAuth = Cookies.get('isGitHubAuthEnabled'); + + if (savedGitHubAuth) { + isGitHubAuthEnabled.set(savedGitHubAuth === 'true'); + } }, []); // writing values to cookies on change @@ -111,6 +120,20 @@ export function useSettings() { Cookies.set('isLocalModelsEnabled', String(enabled)); }, []); + const enableGitHubAuth = useCallback((enabled: boolean) => { + isGitHubAuthEnabled.set(enabled); + logStore.logSystem(`GitHub authentication ${enabled ? 'enabled' : 'disabled'}`); + Cookies.set('isGitHubAuthEnabled', String(enabled)); + + // Clean up GitHub data when feature is disabled + if (!enabled) { + localStorage.removeItem('github_token'); + Cookies.remove('githubUsername'); + Cookies.remove('githubToken'); + Cookies.remove('git:github.com'); + } + }, []); + return { providers, activeProviders, @@ -121,5 +144,7 @@ export function useSettings() { enableEventLogs, isLocalModel, enableLocalModels, + isGitHubAuth, + enableGitHubAuth, }; } diff --git a/app/lib/stores/settings.ts b/app/lib/stores/settings.ts index abbb825d3..344d925b8 100644 --- a/app/lib/stores/settings.ts +++ b/app/lib/stores/settings.ts @@ -46,3 +46,5 @@ export const isDebugMode = atom(false); export const isEventLogsEnabled = atom(false); export const isLocalModelsEnabled = atom(true); + +export const isGitHubAuthEnabled = atom(false); diff --git a/app/routes/api.github.proxy.ts b/app/routes/api.github.proxy.ts index 813111f1b..364f3ca21 100644 --- a/app/routes/api.github.proxy.ts +++ b/app/routes/api.github.proxy.ts @@ -39,14 +39,26 @@ export async function action({ request }: ActionFunctionArgs) { const data = await response.json(); + // Check if the response is an error + if (data.error) { + return new Response(JSON.stringify(data), { + status: data.error === 'authorization_pending' ? 202 : 400, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }); + } + return new Response(JSON.stringify(data), { + status: 200, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', }, }); - } catch { - return new Response(JSON.stringify({ error: 'Failed to proxy request' }), { + } catch (error) { + return new Response(JSON.stringify({ error: 'Failed to proxy request', details: error.message }), { status: 500, headers: { 'Content-Type': 'application/json', From ea6d5e591128c0869455988e1db7246cafe88de8 Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Sat, 14 Dec 2024 14:45:57 -0500 Subject: [PATCH 07/36] GitHub OAuth optional feature --- app/commit.json | 2 +- .../settings/features/FeaturesTab.tsx | 17 +++- app/components/sidebar/Menu.client.tsx | 29 +++++-- app/lib/github/GitHubAuth.tsx | 74 +++++++++------- app/lib/github/github.client.ts | 16 ++-- app/lib/github/useGitHubAuth.ts | 78 ++++++++--------- app/routes/api.github.proxy.ts | 84 +++++++++++++++++-- 7 files changed, 199 insertions(+), 101 deletions(-) diff --git a/app/commit.json b/app/commit.json index 349125ad6..6c49e7d5d 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "e4af820caed90cf25fbbdb7a091bfa32650d24c9" } +{ "commit": "0e2c9fb3d7214da08dd30a491cab7e239f21a7cf" } diff --git a/app/components/settings/features/FeaturesTab.tsx b/app/components/settings/features/FeaturesTab.tsx index 79e588e9b..17ba4e094 100644 --- a/app/components/settings/features/FeaturesTab.tsx +++ b/app/components/settings/features/FeaturesTab.tsx @@ -3,7 +3,16 @@ import { Switch } from '~/components/ui/Switch'; import { useSettings } from '~/lib/hooks/useSettings'; export default function FeaturesTab() { - const { debug, enableDebugMode, isLocalModel, enableLocalModels, eventLogs, enableEventLogs, isGitHubAuth, enableGitHubAuth } = useSettings(); + const { + debug, + enableDebugMode, + isLocalModel, + enableLocalModels, + eventLogs, + enableEventLogs, + isGitHubAuth, + enableGitHubAuth, + } = useSettings(); return (
@@ -21,7 +30,11 @@ export default function FeaturesTab() { GitHub Auth
-

A utility feature that Provides GitHub authentication. If your feature needs GitHub authentication you can use this. The useGitHubAuth() hook provides authentication state including login status, loading state, and user information. Once authenticated, you can access the GitHub token from localStorage.

+

+ A utility feature that Provides GitHub authentication. If your feature needs GitHub authentication you can + use this. The useGitHubAuth() hook provides authentication state including login status, loading state, and + user information. Once authenticated, you can access the GitHub token from localStorage. +

diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx index e054cda3d..7b3ba8bb4 100644 --- a/app/components/sidebar/Menu.client.tsx +++ b/app/components/sidebar/Menu.client.tsx @@ -62,12 +62,15 @@ export const Menu = () => { const [list, setList] = useState([]); const [open, setOpen] = useState(false); const [dialogContent, setDialogContent] = useState(null); - const { isAuthenticated, handleAuthComplete, handleLogout } = useGitHubAuth(); + const { isAuthenticated, handleAuthComplete, handleLogout, isLoading } = useGitHubAuth(); const [isSettingsOpen, setIsSettingsOpen] = useState(false); - const handleGitHubAuthComplete = useCallback((token: string) => { - handleAuthComplete(token); - }, [handleAuthComplete]); + const handleGitHubAuthComplete = useCallback( + (token: string) => { + handleAuthComplete(token); + }, + [handleAuthComplete], + ); const handleGitHubError = useCallback((error: Error) => { toast.error(error.message); @@ -168,7 +171,17 @@ export const Menu = () => { Start new chat - {isAuthenticated ? ( + {isLoading ? ( + + ) : isAuthenticated ? (
{isPolling && ( @@ -188,7 +198,7 @@ export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProp return React.cloneElement(children as React.ReactElement, { onClick: (e: React.MouseEvent) => { e.preventDefault(); - initializeAuth(); + handleStartAuth(); (children as React.ReactElement).props.onClick?.(e); }, }); diff --git a/app/lib/github/github.client.ts b/app/lib/github/github.client.ts index e183313b9..8df941f89 100644 --- a/app/lib/github/github.client.ts +++ b/app/lib/github/github.client.ts @@ -1,23 +1,23 @@ -import { GITHUB_CONFIG } from './config'; - export interface GitHubUser { login: string; id: number; avatar_url: string; - name: string; - email: string; + name?: string; + email?: string; } -export async function getGitHubUser(accessToken: string): Promise { - const response = await fetch(GITHUB_CONFIG.userApiUrl, { +export async function getGitHubUser(token: string): Promise { + const response = await fetch('https://api.github.com/user', { headers: { - Authorization: `Bearer ${accessToken}`, + Authorization: `Bearer ${token}`, Accept: 'application/json', }, }); if (!response.ok) { - throw new Error('Failed to get user info'); + const error = new Error('Failed to get user info') as any; + error.status = response.status; + throw error; } return response.json(); diff --git a/app/lib/github/useGitHubAuth.ts b/app/lib/github/useGitHubAuth.ts index 2833bdbca..a9788a036 100644 --- a/app/lib/github/useGitHubAuth.ts +++ b/app/lib/github/useGitHubAuth.ts @@ -3,64 +3,58 @@ import { getGitHubUser, type GitHubUser } from './github.client'; import { isGitHubAuthEnabled } from '~/lib/stores/settings'; import { useStore } from '@nanostores/react'; -// Create a global variable to cache the auth state -let cachedUser: GitHubUser | null = null; -let cachedIsAuthenticated = false; - export function useGitHubAuth() { - const [isAuthenticated, setIsAuthenticated] = useState(cachedIsAuthenticated); - const [isLoading, setIsLoading] = useState(!cachedUser); - const [user, setUser] = useState(cachedUser); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [user, setUser] = useState(null); const isGitHubAuth = useStore(isGitHubAuthEnabled); const checkAuth = useCallback(async () => { // If GitHub auth is disabled, don't authenticate if (!isGitHubAuth) { - if (isAuthenticated) { // Only clear if we were previously authenticated + if (isAuthenticated) { + // Only clear if we were previously authenticated setIsAuthenticated(false); setUser(null); - cachedUser = null; - cachedIsAuthenticated = false; localStorage.removeItem('github_token'); } + setIsLoading(false); + return; } const token = localStorage.getItem('github_token'); - if (token) { - try { - const userInfo = await getGitHubUser(token); - setUser(userInfo); - setIsAuthenticated(true); - cachedUser = userInfo; - cachedIsAuthenticated = true; - } catch (error) { - // Only remove token if it's an auth error (401 or 403) - if (error instanceof Error && 'status' in error && (error.status === 401 || error.status === 403)) { - localStorage.removeItem('github_token'); - } - setIsAuthenticated(false); - setUser(null); - cachedUser = null; - cachedIsAuthenticated = false; + if (!token) { + setIsAuthenticated(false); + setUser(null); + setIsLoading(false); + + return; + } + + try { + const userInfo = await getGitHubUser(token); + setUser(userInfo); + setIsAuthenticated(true); + } catch (error) { + // Only remove token if it's an auth error (401 or 403) + if (error instanceof Error && 'status' in error && (error.status === 401 || error.status === 403)) { + localStorage.removeItem('github_token'); } - } else { + setIsAuthenticated(false); setUser(null); - cachedUser = null; - cachedIsAuthenticated = false; + } finally { + setIsLoading(false); } - setIsLoading(false); }, [isGitHubAuth, isAuthenticated]); // Initial auth check useEffect(() => { - if (!cachedUser || !cachedIsAuthenticated) { - checkAuth(); - } - }, []); // Only run on mount + checkAuth(); + }, [checkAuth]); // Handle GitHub auth toggle useEffect(() => { @@ -68,8 +62,6 @@ export function useGitHubAuth() { // Clear auth when feature is disabled setIsAuthenticated(false); setUser(null); - cachedUser = null; - cachedIsAuthenticated = false; localStorage.removeItem('github_token'); } else if (isGitHubAuth && !isAuthenticated) { // Try to authenticate when feature is enabled @@ -80,6 +72,7 @@ export function useGitHubAuth() { // Re-run auth check when window regains focus useEffect(() => { window.addEventListener('focus', checkAuth); + return () => { window.removeEventListener('focus', checkAuth); }; @@ -90,12 +83,11 @@ export function useGitHubAuth() { const userInfo = await getGitHubUser(token); setUser(userInfo); setIsAuthenticated(true); - cachedUser = userInfo; - cachedIsAuthenticated = true; - } catch (_error) { - console.error('Failed to get user info:', _error); - cachedUser = null; - cachedIsAuthenticated = false; + } catch (error) { + localStorage.removeItem('github_token'); + setIsAuthenticated(false); + setUser(null); + throw error; } }, []); @@ -103,8 +95,6 @@ export function useGitHubAuth() { localStorage.removeItem('github_token'); setUser(null); setIsAuthenticated(false); - cachedUser = null; - cachedIsAuthenticated = false; }, []); return { diff --git a/app/routes/api.github.proxy.ts b/app/routes/api.github.proxy.ts index 364f3ca21..50345fa62 100644 --- a/app/routes/api.github.proxy.ts +++ b/app/routes/api.github.proxy.ts @@ -1,4 +1,66 @@ -import type { ActionFunctionArgs } from '@remix-run/cloudflare'; +import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/cloudflare'; + +interface GitHubErrorResponse { + error?: string; + error_description?: string; +} + +interface GitHubResponse extends GitHubErrorResponse { + [key: string]: unknown; +} + +export async function loader({ request }: LoaderFunctionArgs) { + const url = new URL(request.url); + const targetEndpoint = url.searchParams.get('endpoint'); + const clientId = url.searchParams.get('client_id'); + + if (!targetEndpoint || !clientId) { + return new Response('Missing required parameters', { status: 400 }); + } + + const githubUrl = `https://github.com${targetEndpoint}`; + const params = new URLSearchParams(); + + // Forward all query parameters to GitHub + url.searchParams.forEach((value, key) => { + if (key !== 'endpoint') { + params.append(key, value); + } + }); + + try { + const response = await fetch(`${githubUrl}?${params}`, { + method: 'GET', + headers: { + Accept: 'application/json', + }, + }); + + const data = (await response.json()) as GitHubResponse; + + return new Response(JSON.stringify(data), { + status: response.status, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }); + } catch (error) { + return new Response( + JSON.stringify({ + error: 'Failed to proxy request', + details: error instanceof Error ? error.message : 'Unknown error', + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, + }, + ); + } +} export async function action({ request }: ActionFunctionArgs) { const url = new URL(request.url); @@ -37,7 +99,7 @@ export async function action({ request }: ActionFunctionArgs) { body: JSON.stringify(body), }); - const data = await response.json(); + const data = (await response.json()) as GitHubResponse; // Check if the response is an error if (data.error) { @@ -58,13 +120,19 @@ export async function action({ request }: ActionFunctionArgs) { }, }); } catch (error) { - return new Response(JSON.stringify({ error: 'Failed to proxy request', details: error.message }), { - status: 500, - headers: { - 'Content-Type': 'application/json', - 'Access-Control-Allow-Origin': '*', + return new Response( + JSON.stringify({ + error: 'Failed to proxy request', + details: error instanceof Error ? error.message : 'Unknown error', + }), + { + status: 500, + headers: { + 'Content-Type': 'application/json', + 'Access-Control-Allow-Origin': '*', + }, }, - }); + ); } } From e91bd2b17690a611dd3dd4707cd4c24d00d6a49f Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Sat, 14 Dec 2024 14:49:34 -0500 Subject: [PATCH 08/36] merge changes --- app/commit.json | 2 +- app/utils/constants.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/commit.json b/app/commit.json index 4fe293e36..15662a249 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "ea6d5e591128c0869455988e1db7246cafe88de8" } +{ "commit": "5497f8faccab69b76e8d97fb0c266ae7821c3657" } diff --git a/app/utils/constants.ts b/app/utils/constants.ts index 6a077f88a..cc2aafc93 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -141,7 +141,7 @@ const PROVIDER_LIST: ProviderInfo[] = [ staticModels: [ { name: 'llama-3.1-8b-instant', label: 'Llama 3.1 8b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 }, { name: 'llama-3.2-11b-vision-preview', label: 'Llama 3.2 11b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 }, - { name: 'llama-3.2-90b-vision-preview', label: 'Llama 3.2 90b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 }, + { name: 'llama-3.2-90b-vision-preview', label: 'Llama 3.2 90b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 }, { name: 'llama-3.2-3b-preview', label: 'Llama 3.2 3b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 }, { name: 'llama-3.2-1b-preview', label: 'Llama 3.2 1b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 }, { name: 'llama-3.3-70b-versatile', label: 'Llama 3.3 70b (Groq)', provider: 'Groq', maxTokenAllowed: 8000 }, From 66beb24527f841120a8d3372b01265fc47509dc7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 14 Dec 2024 21:05:02 +0000 Subject: [PATCH 09/36] chore: update commit hash to 9efc709782ed44a36da6de2222b1d5dd004fb489 --- app/commit.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/commit.json b/app/commit.json index 1636c99bd..c731689e5 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "ece0213500a94a6b29e29512c5040baf57884014" } +{ "commit": "9efc709782ed44a36da6de2222b1d5dd004fb489" } From 865324d176cefa479d148d56880ece879ab8c10c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 20 Dec 2024 00:50:25 +0000 Subject: [PATCH 10/36] chore: update commit hash to 2638c1a704118b411f942e1b17b6765abce46721 --- app/commit.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/commit.json b/app/commit.json index 0b3f44a2c..225cf9b6e 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "636f87f568a368dadc5cf3c077284710951e2488", "version": "0.0.3" } +{ "commit": "2638c1a704118b411f942e1b17b6765abce46721", "version": "0.0.3" } From 1a4d3005fe7e8e87fca02028bac27895318bf3ae Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Thu, 19 Dec 2024 19:56:03 -0500 Subject: [PATCH 11/36] commit --- app/commit.json | 2 +- .../settings/providers/ProvidersTab.tsx | 21 ++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/app/commit.json b/app/commit.json index a58b9937a..ace727516 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "66beb24527f841120a8d3372b01265fc47509dc7" } +{ "commit": "54e71569d4cb60fb61567645bea7f1c8a779215c" } diff --git a/app/components/settings/providers/ProvidersTab.tsx b/app/components/settings/providers/ProvidersTab.tsx index 58c8dac6b..e03731f43 100644 --- a/app/components/settings/providers/ProvidersTab.tsx +++ b/app/components/settings/providers/ProvidersTab.tsx @@ -35,8 +35,8 @@ export default function ProvidersTab() { newFilteredProviders.sort((a, b) => a.name.localeCompare(b.name)); // Split providers into regular and URL-configurable - const regular = newFilteredProviders.filter(p => !URL_CONFIGURABLE_PROVIDERS.includes(p.name)); - const urlConfigurable = newFilteredProviders.filter(p => URL_CONFIGURABLE_PROVIDERS.includes(p.name)); + const regular = newFilteredProviders.filter((p) => !URL_CONFIGURABLE_PROVIDERS.includes(p.name)); + const urlConfigurable = newFilteredProviders.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name)); setFilteredProviders([...regular, ...urlConfigurable]); }, [providers, searchTerm, isLocalModel]); @@ -112,8 +112,8 @@ export default function ProvidersTab() { ); }; - const regularProviders = filteredProviders.filter(p => !URL_CONFIGURABLE_PROVIDERS.includes(p.name)); - const urlConfigurableProviders = filteredProviders.filter(p => URL_CONFIGURABLE_PROVIDERS.includes(p.name)); + const regularProviders = filteredProviders.filter((p) => !URL_CONFIGURABLE_PROVIDERS.includes(p.name)); + const urlConfigurableProviders = filteredProviders.filter((p) => URL_CONFIGURABLE_PROVIDERS.includes(p.name)); return (
@@ -128,22 +128,19 @@ export default function ProvidersTab() {
{/* Regular Providers Grid */} -
- {regularProviders.map(renderProviderCard)} -
+
{regularProviders.map(renderProviderCard)}
{/* URL Configurable Providers Section */} {urlConfigurableProviders.length > 0 && (

Experimental Providers

- These providers are experimental and allow you to run AI models locally or connect to your own infrastructure. They require additional setup but offer more flexibility. + These providers are experimental and allow you to run AI models locally or connect to your own + infrastructure. They require additional setup but offer more flexibility.

-
- {urlConfigurableProviders.map(renderProviderCard)} -
+
{urlConfigurableProviders.map(renderProviderCard)}
)}
); -} \ No newline at end of file +} From 93d121cc8eb127729b51809bc3a35a98fb89f676 Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Thu, 19 Dec 2024 20:25:44 -0500 Subject: [PATCH 12/36] Merge branch 'main' into feat/git-auth --- app/commit.json | 2 +- .../settings/features/FeaturesTab.tsx | 33 +++++++++++++++---- app/lib/hooks/useSettings.tsx | 11 ++++--- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/app/commit.json b/app/commit.json index 0e9185538..203772bf8 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "e91bd2b17690a611dd3dd4707cd4c24d00d6a49f" } +{ "commit": "4a55c2b120e69e645dfdaae13e667d7e5152de77" } diff --git a/app/components/settings/features/FeaturesTab.tsx b/app/components/settings/features/FeaturesTab.tsx index bc98459c9..ce9044699 100644 --- a/app/components/settings/features/FeaturesTab.tsx +++ b/app/components/settings/features/FeaturesTab.tsx @@ -27,13 +27,31 @@ export default function FeaturesTab() {

Optional Features

-
- Debug Info - +
+
+ Debug Features + +
+
+
+ Use Main Branch +

+ Check for updates against the main branch instead of stable +

+
+ +
-
- Event Logs - +
+
+ GitHub Auth + +
+

+ A utility feature that Provides GitHub authentication. If your feature needs GitHub authentication you can + use this. The useGitHubAuth() hook provides authentication state including login status, loading state, and + user information. Once authenticated, you can access the GitHub token from localStorage. +

@@ -54,7 +72,8 @@ export default function FeaturesTab() { Choose a prompt from the library to use as the system prompt.

- setRepoName(e.target.value)} + placeholder="Repository name" + className="w-full px-4 py-2 rounded-lg border border-bolt-elements-border bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + +
+ + )}
diff --git a/app/components/workbench/Workbench.client.tsx b/app/components/workbench/Workbench.client.tsx index 22d88ed99..27db74145 100644 --- a/app/components/workbench/Workbench.client.tsx +++ b/app/components/workbench/Workbench.client.tsx @@ -178,29 +178,14 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => const existingToken = localStorage.getItem('github_token'); if (existingToken) { - // Get the GitHub user info directly - const user = await getGitHubUser(existingToken); - - // Prompt for repository name - const repoName = prompt( - 'Enter a name for your GitHub repository:', - 'bolt-generated-project', - ); - - if (!repoName) { - alert('Repository name is required. Push to GitHub cancelled.'); - return; - } - - workbenchStore.pushToGitHub(repoName, user.login, existingToken); - } else { - // No token, show the auth modal - setIsAuthModalOpen(true); + // Get the GitHub user info directly to validate token + await getGitHubUser(existingToken); } + // Show auth modal in both cases - it will handle the flow + setIsAuthModalOpen(true); } catch (error) { console.error('Failed to use existing GitHub token:', error); - - // If token is invalid, show the auth modal + // If token is invalid, remove it localStorage.removeItem('github_token'); setIsAuthModalOpen(true); } @@ -251,22 +236,9 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => setIsAuthModalOpen(false)} - onAuthComplete={async (token: string) => { - try { - const user = await getGitHubUser(token); - const repoName = prompt('Please enter a name for your new GitHub repository:', 'bolt-generated-project'); - - if (!repoName) { - alert('Repository name is required. Push to GitHub cancelled.'); - return; - } - - workbenchStore.pushToGitHub(repoName, user.login, token); - setIsAuthModalOpen(false); - } catch (error) { - console.error('Failed to get GitHub user:', error); - alert('Failed to get GitHub user info. Please try again.'); - } + onAuthComplete={(token: string) => { + // Token is already stored in localStorage by GitHubAuth component + setIsAuthModalOpen(false); }} /> From 9609a5199ab41468f412a55f4f2b2e824b3cfd32 Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Fri, 20 Dec 2024 11:32:00 -0500 Subject: [PATCH 16/36] more changes --- app/commit.json | 2 +- app/components/github/GitHubAuthModal.tsx | 8 ++++++-- app/components/workbench/Workbench.client.tsx | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/commit.json b/app/commit.json index 98266091b..713371084 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "087e02ee8c80e6157532e3ddc844bc96db379391" } +{ "commit": "f7397133fc6304be8b6a0f94f03b06cd159cf61c" } diff --git a/app/components/github/GitHubAuthModal.tsx b/app/components/github/GitHubAuthModal.tsx index bdc863694..225cc46e5 100644 --- a/app/components/github/GitHubAuthModal.tsx +++ b/app/components/github/GitHubAuthModal.tsx @@ -35,7 +35,9 @@ export function GitHubAuthModal({ isOpen, onClose, onAuthComplete }: GitHubAuthM return; } - if (!user || !token) return; + if (!user || !token) { + return; + } workbenchStore.pushToGitHub(repoName, user.login, token); onAuthComplete?.(token); @@ -64,7 +66,9 @@ export function GitHubAuthModal({ isOpen, onClose, onAuthComplete }: GitHubAuthM

GitHub Authentication

{!isAuthenticated ? ( <> -

Authenticate with GitHub to push your project

+

+ Authenticate with GitHub to push your project +

{error &&
{error}
} diff --git a/app/components/workbench/Workbench.client.tsx b/app/components/workbench/Workbench.client.tsx index 27db74145..9875b68d6 100644 --- a/app/components/workbench/Workbench.client.tsx +++ b/app/components/workbench/Workbench.client.tsx @@ -181,10 +181,12 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => // Get the GitHub user info directly to validate token await getGitHubUser(existingToken); } + // Show auth modal in both cases - it will handle the flow setIsAuthModalOpen(true); } catch (error) { console.error('Failed to use existing GitHub token:', error); + // If token is invalid, remove it localStorage.removeItem('github_token'); setIsAuthModalOpen(true); @@ -236,7 +238,7 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => setIsAuthModalOpen(false)} - onAuthComplete={(token: string) => { + onAuthComplete={() => { // Token is already stored in localStorage by GitHubAuth component setIsAuthModalOpen(false); }} From 43d5f280c676827ef98f85de4b961db389527052 Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Fri, 20 Dec 2024 11:42:57 -0500 Subject: [PATCH 17/36] more changes --- app/commit.json | 2 +- app/components/github/GitHubAuthModal.tsx | 23 +++++++++++++++++-- app/components/workbench/Workbench.client.tsx | 3 ++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app/commit.json b/app/commit.json index 713371084..cbdd99de3 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "f7397133fc6304be8b6a0f94f03b06cd159cf61c" } +{ "commit": "9609a5199ab41468f412a55f4f2b2e824b3cfd32" } diff --git a/app/components/github/GitHubAuthModal.tsx b/app/components/github/GitHubAuthModal.tsx index 225cc46e5..b3846d3a2 100644 --- a/app/components/github/GitHubAuthModal.tsx +++ b/app/components/github/GitHubAuthModal.tsx @@ -8,15 +8,32 @@ interface GitHubAuthModalProps { isOpen: boolean; onClose: () => void; onAuthComplete?: (token: string) => void; + initialToken?: string | null; } -export function GitHubAuthModal({ isOpen, onClose, onAuthComplete }: GitHubAuthModalProps) { +export function GitHubAuthModal({ isOpen, onClose, onAuthComplete, initialToken }: GitHubAuthModalProps) { const [error, setError] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); const [repoName, setRepoName] = useState('bolt-generated-project'); const [user, setUser] = useState<{ login: string } | null>(null); const [token, setToken] = useState(null); + // If we have an initial token, validate and use it + useEffect(() => { + if (initialToken && !isAuthenticated) { + getGitHubUser(initialToken) + .then((githubUser) => { + setUser(githubUser); + setToken(initialToken); + setIsAuthenticated(true); + }) + .catch((error) => { + console.error('Failed to validate token:', error); + setError('Failed to validate GitHub token. Please authenticate again.'); + }); + } + }, [initialToken, isAuthenticated]); + const handleAuthComplete = useCallback(async (authToken: string) => { try { const githubUser = await getGitHubUser(authToken); @@ -63,7 +80,9 @@ export function GitHubAuthModal({ isOpen, onClose, onAuthComplete }: GitHubAuthM
-

GitHub Authentication

+

+ {isAuthenticated ? 'Create GitHub Repository' : 'GitHub Authentication'} +

{!isAuthenticated ? ( <>

diff --git a/app/components/workbench/Workbench.client.tsx b/app/components/workbench/Workbench.client.tsx index 9875b68d6..454e84dcd 100644 --- a/app/components/workbench/Workbench.client.tsx +++ b/app/components/workbench/Workbench.client.tsx @@ -182,7 +182,7 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => await getGitHubUser(existingToken); } - // Show auth modal in both cases - it will handle the flow + // Show auth modal, passing the existing token if we have one setIsAuthModalOpen(true); } catch (error) { console.error('Failed to use existing GitHub token:', error); @@ -242,6 +242,7 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => // Token is already stored in localStorage by GitHubAuth component setIsAuthModalOpen(false); }} + initialToken={localStorage.getItem('github_token')} /> ) From 733fc830de92af0488360a0f8ff07838e9d99b91 Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Fri, 20 Dec 2024 11:57:01 -0500 Subject: [PATCH 18/36] more changes --- app/commit.json | 2 +- app/components/github/GitHubAuthModal.tsx | 17 +++++--- app/components/workbench/Workbench.client.tsx | 39 ++++++++++++++++++- app/lib/stores/workbench.ts | 4 +- 4 files changed, 52 insertions(+), 10 deletions(-) diff --git a/app/commit.json b/app/commit.json index cbdd99de3..6bf73b7fd 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "9609a5199ab41468f412a55f4f2b2e824b3cfd32" } +{ "commit": "43d5f280c676827ef98f85de4b961db389527052" } diff --git a/app/components/github/GitHubAuthModal.tsx b/app/components/github/GitHubAuthModal.tsx index b3846d3a2..8778cfd4c 100644 --- a/app/components/github/GitHubAuthModal.tsx +++ b/app/components/github/GitHubAuthModal.tsx @@ -8,10 +8,11 @@ interface GitHubAuthModalProps { isOpen: boolean; onClose: () => void; onAuthComplete?: (token: string) => void; + onPushComplete?: (success: boolean, repoUrl?: string) => void; initialToken?: string | null; } -export function GitHubAuthModal({ isOpen, onClose, onAuthComplete, initialToken }: GitHubAuthModalProps) { +export function GitHubAuthModal({ isOpen, onClose, onAuthComplete, onPushComplete, initialToken }: GitHubAuthModalProps) { const [error, setError] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); const [repoName, setRepoName] = useState('bolt-generated-project'); @@ -46,7 +47,7 @@ export function GitHubAuthModal({ isOpen, onClose, onAuthComplete, initialToken } }, []); - const handleCreateRepo = useCallback(() => { + const handleCreateRepo = useCallback(async () => { if (!repoName.trim()) { setError('Repository name is required'); return; @@ -56,10 +57,16 @@ export function GitHubAuthModal({ isOpen, onClose, onAuthComplete, initialToken return; } - workbenchStore.pushToGitHub(repoName, user.login, token); onAuthComplete?.(token); - onClose(); - }, [repoName, user, token, onAuthComplete, onClose]); + + try { + const result = await workbenchStore.pushToGitHub(repoName, user.login, token); + onPushComplete?.(true, result.html_url); + } catch (error) { + console.error('Failed to push to GitHub:', error); + onPushComplete?.(false); + } + }, [repoName, user, token, onAuthComplete, onPushComplete]); const handleError = useCallback((error: Error) => { setError(error.message); diff --git a/app/components/workbench/Workbench.client.tsx b/app/components/workbench/Workbench.client.tsx index 454e84dcd..2fea47e30 100644 --- a/app/components/workbench/Workbench.client.tsx +++ b/app/components/workbench/Workbench.client.tsx @@ -19,6 +19,7 @@ import { Preview } from './Preview'; import useViewport from '~/lib/hooks'; import { GitHubAuthModal } from '~/components/github/GitHubAuthModal'; import { getGitHubUser } from '~/lib/github/github.client'; +import { LoadingDots } from '~/components/ui/LoadingDots'; interface WorkspaceProps { chatStarted?: boolean; @@ -60,6 +61,7 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => const [isSyncing, setIsSyncing] = useState(false); const [isAuthModalOpen, setIsAuthModalOpen] = useState(false); + const [isPushingToGitHub, setIsPushingToGitHub] = useState(false); const hasPreview = useStore(computed(workbenchStore.previews, (previews) => previews.length > 0)); const showWorkbench = useStore(workbenchStore.showWorkbench); @@ -238,12 +240,45 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => setIsAuthModalOpen(false)} - onAuthComplete={() => { - // Token is already stored in localStorage by GitHubAuth component + onAuthComplete={async (token: string) => { setIsAuthModalOpen(false); + setIsPushingToGitHub(true); + }} + onPushComplete={(success: boolean, repoUrl?: string) => { + setIsPushingToGitHub(false); + if (success) { + toast.success( +

+ Successfully pushed to GitHub! + {repoUrl && ( + + View Repository → + + )} +
, + { autoClose: 5000 } + ); + } else { + toast.error('Failed to push to GitHub. Please try again.'); + } }} initialToken={localStorage.getItem('github_token')} /> + + {/* Loading Overlay */} + {isPushingToGitHub && ( +
+
+
+

Pushing your project to GitHub...

+
+
+ )} ) ); diff --git a/app/lib/stores/workbench.ts b/app/lib/stores/workbench.ts index 0d46057db..df1aa6e03 100644 --- a/app/lib/stores/workbench.ts +++ b/app/lib/stores/workbench.ts @@ -510,10 +510,10 @@ export class WorkbenchStore { sha: newCommit.sha, }); - alert(`Repository created and code pushed: ${repo.html_url}`); + return repo; // Return the repo object instead of showing an alert } catch (error) { console.error('Error pushing to GitHub:', error); - throw error; // Rethrow the error for further handling + throw error; } } } From f0464a273e482d0c0d051a2918b650dae9c35f64 Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Fri, 20 Dec 2024 11:57:19 -0500 Subject: [PATCH 19/36] more changes --- app/commit.json | 2 +- app/components/github/GitHubAuthModal.tsx | 8 +++++++- app/components/workbench/Workbench.client.tsx | 6 +++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/commit.json b/app/commit.json index 6bf73b7fd..bd0bb1b7a 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "43d5f280c676827ef98f85de4b961db389527052" } +{ "commit": "733fc830de92af0488360a0f8ff07838e9d99b91" } diff --git a/app/components/github/GitHubAuthModal.tsx b/app/components/github/GitHubAuthModal.tsx index 8778cfd4c..b07a38880 100644 --- a/app/components/github/GitHubAuthModal.tsx +++ b/app/components/github/GitHubAuthModal.tsx @@ -12,7 +12,13 @@ interface GitHubAuthModalProps { initialToken?: string | null; } -export function GitHubAuthModal({ isOpen, onClose, onAuthComplete, onPushComplete, initialToken }: GitHubAuthModalProps) { +export function GitHubAuthModal({ + isOpen, + onClose, + onAuthComplete, + onPushComplete, + initialToken, +}: GitHubAuthModalProps) { const [error, setError] = useState(null); const [isAuthenticated, setIsAuthenticated] = useState(false); const [repoName, setRepoName] = useState('bolt-generated-project'); diff --git a/app/components/workbench/Workbench.client.tsx b/app/components/workbench/Workbench.client.tsx index 2fea47e30..590f72b48 100644 --- a/app/components/workbench/Workbench.client.tsx +++ b/app/components/workbench/Workbench.client.tsx @@ -19,7 +19,6 @@ import { Preview } from './Preview'; import useViewport from '~/lib/hooks'; import { GitHubAuthModal } from '~/components/github/GitHubAuthModal'; import { getGitHubUser } from '~/lib/github/github.client'; -import { LoadingDots } from '~/components/ui/LoadingDots'; interface WorkspaceProps { chatStarted?: boolean; @@ -240,12 +239,13 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => setIsAuthModalOpen(false)} - onAuthComplete={async (token: string) => { + onAuthComplete={async () => { setIsAuthModalOpen(false); setIsPushingToGitHub(true); }} onPushComplete={(success: boolean, repoUrl?: string) => { setIsPushingToGitHub(false); + if (success) { toast.success(
@@ -261,7 +261,7 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => )}
, - { autoClose: 5000 } + { autoClose: 5000 }, ); } else { toast.error('Failed to push to GitHub. Please try again.'); From dad7562c857a9198998d07f97b25d11f14d9de98 Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Fri, 20 Dec 2024 12:11:08 -0500 Subject: [PATCH 20/36] more changes --- app/commit.json | 2 +- app/components/github/GitHubAuthModal.tsx | 10 +++++----- app/components/ui/Dialog.tsx | 2 +- app/components/workbench/Workbench.client.tsx | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/commit.json b/app/commit.json index bd0bb1b7a..c1ef894b0 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "733fc830de92af0488360a0f8ff07838e9d99b91" } +{ "commit": "f0464a273e482d0c0d051a2918b650dae9c35f64" } diff --git a/app/components/github/GitHubAuthModal.tsx b/app/components/github/GitHubAuthModal.tsx index b07a38880..b962aed46 100644 --- a/app/components/github/GitHubAuthModal.tsx +++ b/app/components/github/GitHubAuthModal.tsx @@ -91,10 +91,10 @@ export function GitHubAuthModal({ return ( - -
+ +

- {isAuthenticated ? 'Create GitHub Repository' : 'GitHub Authentication'} + {isAuthenticated ? 'Push GitHub Repository' : 'GitHub Authentication'}

{!isAuthenticated ? ( <> @@ -114,11 +114,11 @@ export function GitHubAuthModal({ value={repoName} onChange={(e) => setRepoName(e.target.value)} placeholder="Repository name" - className="w-full px-4 py-2 rounded-lg border border-bolt-elements-border bg-bolt-elements-background-depth-1 text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-blue-500" + className="w-full px-4 py-2 rounded-lg border border-gray-700 bg-[#1A1A1A] text-bolt-elements-textPrimary focus:outline-none focus:ring-1 focus:ring-gray-600 focus:border-transparent" /> diff --git a/app/components/ui/Dialog.tsx b/app/components/ui/Dialog.tsx index a808c7742..d19a4375d 100644 --- a/app/components/ui/Dialog.tsx +++ b/app/components/ui/Dialog.tsx @@ -114,7 +114,7 @@ export const Dialog = memo(({ className, children, onBackdrop, onClose }: Dialog {/* Loading Overlay */} {isPushingToGitHub && (
-
-
+
+

Pushing your project to GitHub...

From addeefef5724e43dbdaeaa41372e7f256f45f5ee Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Fri, 20 Dec 2024 12:26:51 -0500 Subject: [PATCH 21/36] more changes --- app/commit.json | 2 +- app/components/github/GitHubAuthModal.tsx | 4 +--- app/components/workbench/Workbench.client.tsx | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/app/commit.json b/app/commit.json index c1ef894b0..5713e8958 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "f0464a273e482d0c0d051a2918b650dae9c35f64" } +{ "commit": "dad7562c857a9198998d07f97b25d11f14d9de98" } diff --git a/app/components/github/GitHubAuthModal.tsx b/app/components/github/GitHubAuthModal.tsx index b962aed46..f68ddc98e 100644 --- a/app/components/github/GitHubAuthModal.tsx +++ b/app/components/github/GitHubAuthModal.tsx @@ -91,8 +91,7 @@ export function GitHubAuthModal({ return ( - -
+

{isAuthenticated ? 'Push GitHub Repository' : 'GitHub Authentication'}

@@ -125,7 +124,6 @@ export function GitHubAuthModal({
)} -
); diff --git a/app/components/workbench/Workbench.client.tsx b/app/components/workbench/Workbench.client.tsx index e5a6a1e74..b7c74014a 100644 --- a/app/components/workbench/Workbench.client.tsx +++ b/app/components/workbench/Workbench.client.tsx @@ -273,7 +273,7 @@ export const Workbench = memo(({ chatStarted, isStreaming }: WorkspaceProps) => {/* Loading Overlay */} {isPushingToGitHub && (
-
+

Pushing your project to GitHub...

From 6bd33c9a29a765f4b617f6d326b9230aedb4440f Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Fri, 20 Dec 2024 12:27:11 -0500 Subject: [PATCH 22/36] more changes --- app/commit.json | 2 +- app/components/github/GitHubAuthModal.tsx | 69 ++++++++++++----------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/app/commit.json b/app/commit.json index 5713e8958..1bbe57b34 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "dad7562c857a9198998d07f97b25d11f14d9de98" } +{ "commit": "addeefef5724e43dbdaeaa41372e7f256f45f5ee" } diff --git a/app/components/github/GitHubAuthModal.tsx b/app/components/github/GitHubAuthModal.tsx index f68ddc98e..a15bc5333 100644 --- a/app/components/github/GitHubAuthModal.tsx +++ b/app/components/github/GitHubAuthModal.tsx @@ -91,39 +91,42 @@ export function GitHubAuthModal({ return ( - -

- {isAuthenticated ? 'Push GitHub Repository' : 'GitHub Authentication'} -

- {!isAuthenticated ? ( - <> -

- Authenticate with GitHub to push your project -

- {error &&
{error}
} - - - ) : ( - <> -

Enter a name for your GitHub repository

- {error &&
{error}
} -
- setRepoName(e.target.value)} - placeholder="Repository name" - className="w-full px-4 py-2 rounded-lg border border-gray-700 bg-[#1A1A1A] text-bolt-elements-textPrimary focus:outline-none focus:ring-1 focus:ring-gray-600 focus:border-transparent" - /> - -
- - )} + +

+ {isAuthenticated ? 'Push GitHub Repository' : 'GitHub Authentication'} +

+ {!isAuthenticated ? ( + <> +

+ Authenticate with GitHub to push your project +

+ {error &&
{error}
} + + + ) : ( + <> +

Enter a name for your GitHub repository

+ {error &&
{error}
} +
+ setRepoName(e.target.value)} + placeholder="Repository name" + className="w-full px-4 py-2 rounded-lg border border-gray-700 bg-[#1A1A1A] text-bolt-elements-textPrimary focus:outline-none focus:ring-1 focus:ring-gray-600 focus:border-transparent" + /> + +
+ + )}
); From f3f1ea8401cc6d00783cb76fb59805282dbc6347 Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Fri, 20 Dec 2024 13:20:49 -0500 Subject: [PATCH 23/36] more changes --- app/commit.json | 2 +- app/components/github/GitHubAuthModal.tsx | 136 +++++++++++++++------- app/lib/github/GitHubAuth.tsx | 8 +- 3 files changed, 103 insertions(+), 43 deletions(-) diff --git a/app/commit.json b/app/commit.json index 1bbe57b34..a3893f802 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "addeefef5724e43dbdaeaa41372e7f256f45f5ee" } +{ "commit": "6bd33c9a29a765f4b617f6d326b9230aedb4440f" } diff --git a/app/components/github/GitHubAuthModal.tsx b/app/components/github/GitHubAuthModal.tsx index a15bc5333..e42a17d7a 100644 --- a/app/components/github/GitHubAuthModal.tsx +++ b/app/components/github/GitHubAuthModal.tsx @@ -1,8 +1,9 @@ -import { useCallback, useEffect, useState } from 'react'; +import { useCallback, useEffect, useState, useRef } from 'react'; import { Dialog, DialogRoot } from '~/components/ui/Dialog'; import { GitHubAuth } from '~/lib/github/GitHubAuth'; import { getGitHubUser } from '~/lib/github/github.client'; import { workbenchStore } from '~/lib/stores/workbench'; +import { toast } from 'react-toastify'; interface GitHubAuthModalProps { isOpen: boolean; @@ -24,6 +25,8 @@ export function GitHubAuthModal({ const [repoName, setRepoName] = useState('bolt-generated-project'); const [user, setUser] = useState<{ login: string } | null>(null); const [token, setToken] = useState(null); + const [isAuthenticating, setIsAuthenticating] = useState(false); + const hasShownToast = useRef(false); // If we have an initial token, validate and use it useEffect(() => { @@ -42,17 +45,32 @@ export function GitHubAuthModal({ }, [initialToken, isAuthenticated]); const handleAuthComplete = useCallback(async (authToken: string) => { + setIsAuthenticating(true); + try { const githubUser = await getGitHubUser(authToken); setUser(githubUser); setToken(authToken); setIsAuthenticated(true); - } catch (error) { + + if (!hasShownToast.current) { + toast.success('Successfully authenticated with GitHub!'); + hasShownToast.current = true; + } + } catch (error: any) { console.error('Failed to get GitHub user:', error); setError('Failed to get GitHub user info. Please try again.'); + toast.error('Failed to authenticate with GitHub: ' + (error.message || 'Unknown error')); + } finally { + setIsAuthenticating(false); } }, []); + const handleError = useCallback((error: Error) => { + setError(error.message); + toast.error('Failed to authenticate with GitHub: ' + error.message); + }, []); + const handleCreateRepo = useCallback(async () => { if (!repoName.trim()) { setError('Repository name is required'); @@ -74,8 +92,34 @@ export function GitHubAuthModal({ } }, [repoName, user, token, onAuthComplete, onPushComplete]); - const handleError = useCallback((error: Error) => { - setError(error.message); + // Monitor localStorage for GitHub token + useEffect(() => { + if (isAuthenticating) { + const checkToken = () => { + const token = localStorage.getItem('github_token'); + + if (token) { + setIsAuthenticating(false); + handleAuthComplete(token); + } + + return undefined; + }; + + // Check immediately and then set up interval + checkToken(); + + const interval = setInterval(checkToken, 500); + + // Cleanup interval + return () => clearInterval(interval); + } + + return undefined; + }, [isAuthenticating, handleAuthComplete]); + + const startAuth = useCallback(() => { + setIsAuthenticating(true); }, []); // Clear state when modal closes @@ -86,48 +130,58 @@ export function GitHubAuthModal({ setRepoName('bolt-generated-project'); setUser(null); setToken(null); + hasShownToast.current = false; } }, [isOpen]); return ( - -

- {isAuthenticated ? 'Push GitHub Repository' : 'GitHub Authentication'} -

- {!isAuthenticated ? ( - <> -

- Authenticate with GitHub to push your project -

- {error &&
{error}
} - - - ) : ( - <> -

Enter a name for your GitHub repository

- {error &&
{error}
} -
- setRepoName(e.target.value)} - placeholder="Repository name" - className="w-full px-4 py-2 rounded-lg border border-gray-700 bg-[#1A1A1A] text-bolt-elements-textPrimary focus:outline-none focus:ring-1 focus:ring-gray-600 focus:border-transparent" - /> - -
- - )} -
+ {isAuthenticating ? ( +
+
+
+

Authenticating with GitHub...

+
+
+ ) : ( + +

+ {isAuthenticated ? 'Push GitHub Repository' : 'GitHub Authentication'} +

+ {!isAuthenticated ? ( + <> +

+ Authenticate with GitHub to push your project +

+ {error &&
{error}
} + + + ) : ( + <> +

Enter a name for your GitHub repository

+ {error &&
{error}
} +
+ setRepoName(e.target.value)} + placeholder="Repository name" + className="w-full px-4 py-2 rounded-lg border border-gray-700 bg-[#1A1A1A] text-bolt-elements-textPrimary focus:outline-none focus:ring-1 focus:ring-gray-600 focus:border-transparent" + /> + +
+ + )} +
+ )} ); } diff --git a/app/lib/github/GitHubAuth.tsx b/app/lib/github/GitHubAuth.tsx index 15356b525..b9a146561 100644 --- a/app/lib/github/GitHubAuth.tsx +++ b/app/lib/github/GitHubAuth.tsx @@ -5,6 +5,7 @@ import { useSettings } from '~/lib/hooks/useSettings'; interface GitHubAuthProps { onAuthComplete?: (token: string) => void; onError?: (error: Error) => void; + onAuthStart?: () => void; children?: React.ReactNode; } @@ -25,7 +26,7 @@ interface AccessTokenResponse extends GitHubErrorResponse { access_token?: string; } -export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProps) { +export function GitHubAuth({ onAuthComplete, onError, onAuthStart, children }: GitHubAuthProps) { const [isLoading, setIsLoading] = useState(false); const [userCode, setUserCode] = useState(null); const [verificationUrl, setVerificationUrl] = useState(null); @@ -210,6 +211,11 @@ export function GitHubAuth({ onAuthComplete, onError, children }: GitHubAuthProp return React.cloneElement(children as React.ReactElement, { onClick: (e: React.MouseEvent) => { e.preventDefault(); + + if (onAuthStart) { + onAuthStart(); + } + handleStartAuth(); (children as React.ReactElement).props.onClick?.(e); }, From 87579d8370a76abf46ec4c0f540998edb09b08cd Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Fri, 20 Dec 2024 14:51:34 -0500 Subject: [PATCH 24/36] more changes --- app/commit.json | 2 +- .../settings/features/FeaturesTab.tsx | 13 ----- app/components/sidebar/Menu.client.tsx | 51 ------------------- app/lib/github/GitHubAuth.tsx | 6 --- app/lib/github/useGitHubAuth.ts | 32 +----------- app/lib/hooks/useSettings.tsx | 27 +--------- app/lib/stores/settings.ts | 2 - 7 files changed, 3 insertions(+), 130 deletions(-) diff --git a/app/commit.json b/app/commit.json index a3893f802..b0e13098c 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "6bd33c9a29a765f4b617f6d326b9230aedb4440f" } +{ "commit": "f3f1ea8401cc6d00783cb76fb59805282dbc6347" } diff --git a/app/components/settings/features/FeaturesTab.tsx b/app/components/settings/features/FeaturesTab.tsx index ce9044699..7fb4afaea 100644 --- a/app/components/settings/features/FeaturesTab.tsx +++ b/app/components/settings/features/FeaturesTab.tsx @@ -14,8 +14,6 @@ export default function FeaturesTab() { enableLatestBranch, promptId, setPromptId, - isGitHubAuth, - enableGitHubAuth, } = useSettings(); const handleToggle = (enabled: boolean) => { @@ -42,17 +40,6 @@ export default function FeaturesTab() {
-
-
- GitHub Auth - -
-

- A utility feature that Provides GitHub authentication. If your feature needs GitHub authentication you can - use this. The useGitHubAuth() hook provides authentication state including login status, loading state, and - user information. Once authenticated, you can access the GitHub token from localStorage. -

-
diff --git a/app/components/sidebar/Menu.client.tsx b/app/components/sidebar/Menu.client.tsx index 7b3ba8bb4..547a7482d 100644 --- a/app/components/sidebar/Menu.client.tsx +++ b/app/components/sidebar/Menu.client.tsx @@ -10,8 +10,6 @@ import { cubicEasingFn } from '~/utils/easings'; import { logger } from '~/utils/logger'; import { HistoryItem } from './HistoryItem'; import { binDates } from './date-binning'; -import { GitHubAuth } from '~/lib/github/GitHubAuth'; -import { useGitHubAuth } from '~/lib/github/useGitHubAuth'; import { useSearchFilter } from '~/lib/hooks/useSearchFilter'; const menuVariants = { @@ -62,21 +60,8 @@ export const Menu = () => { const [list, setList] = useState([]); const [open, setOpen] = useState(false); const [dialogContent, setDialogContent] = useState(null); - const { isAuthenticated, handleAuthComplete, handleLogout, isLoading } = useGitHubAuth(); const [isSettingsOpen, setIsSettingsOpen] = useState(false); - const handleGitHubAuthComplete = useCallback( - (token: string) => { - handleAuthComplete(token); - }, - [handleAuthComplete], - ); - - const handleGitHubError = useCallback((error: Error) => { - toast.error(error.message); - localStorage.removeItem('github_token'); - }, []); - const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({ items: list, searchFields: ['description'], @@ -171,42 +156,6 @@ export const Menu = () => { Start new chat - {isLoading ? ( - - ) : isAuthenticated ? ( - - ) : ( -
- - - -
- )}
diff --git a/app/lib/github/GitHubAuth.tsx b/app/lib/github/GitHubAuth.tsx index b9a146561..4998cbd50 100644 --- a/app/lib/github/GitHubAuth.tsx +++ b/app/lib/github/GitHubAuth.tsx @@ -1,6 +1,5 @@ import React, { useState, useCallback, useEffect } from 'react'; import { GITHUB_CONFIG } from './config'; -import { useSettings } from '~/lib/hooks/useSettings'; interface GitHubAuthProps { onAuthComplete?: (token: string) => void; @@ -32,7 +31,6 @@ export function GitHubAuth({ onAuthComplete, onError, onAuthStart, children }: G const [verificationUrl, setVerificationUrl] = useState(null); const [isPolling, setIsPolling] = useState(false); const [showCopied, setShowCopied] = useState(false); - const { isGitHubAuth } = useSettings(); // Reset states when auth completes const handleAuthSuccess = useCallback( @@ -157,10 +155,6 @@ export function GitHubAuth({ onAuthComplete, onError, onAuthStart, children }: G } }, [userCode]); - if (!isGitHubAuth) { - return null; - } - if (isLoading) { return (
diff --git a/app/lib/github/useGitHubAuth.ts b/app/lib/github/useGitHubAuth.ts index a9788a036..4a6bb78ca 100644 --- a/app/lib/github/useGitHubAuth.ts +++ b/app/lib/github/useGitHubAuth.ts @@ -1,29 +1,12 @@ import { useCallback, useEffect, useState } from 'react'; import { getGitHubUser, type GitHubUser } from './github.client'; -import { isGitHubAuthEnabled } from '~/lib/stores/settings'; -import { useStore } from '@nanostores/react'; export function useGitHubAuth() { const [isAuthenticated, setIsAuthenticated] = useState(false); const [isLoading, setIsLoading] = useState(true); const [user, setUser] = useState(null); - const isGitHubAuth = useStore(isGitHubAuthEnabled); const checkAuth = useCallback(async () => { - // If GitHub auth is disabled, don't authenticate - if (!isGitHubAuth) { - if (isAuthenticated) { - // Only clear if we were previously authenticated - setIsAuthenticated(false); - setUser(null); - localStorage.removeItem('github_token'); - } - - setIsLoading(false); - - return; - } - const token = localStorage.getItem('github_token'); if (!token) { @@ -49,26 +32,13 @@ export function useGitHubAuth() { } finally { setIsLoading(false); } - }, [isGitHubAuth, isAuthenticated]); + }, []); // Initial auth check useEffect(() => { checkAuth(); }, [checkAuth]); - // Handle GitHub auth toggle - useEffect(() => { - if (!isGitHubAuth && isAuthenticated) { - // Clear auth when feature is disabled - setIsAuthenticated(false); - setUser(null); - localStorage.removeItem('github_token'); - } else if (isGitHubAuth && !isAuthenticated) { - // Try to authenticate when feature is enabled - checkAuth(); - } - }, [isGitHubAuth, isAuthenticated, checkAuth]); - // Re-run auth check when window regains focus useEffect(() => { window.addEventListener('focus', checkAuth); diff --git a/app/lib/hooks/useSettings.tsx b/app/lib/hooks/useSettings.tsx index 75b074c4e..4457fa282 100644 --- a/app/lib/hooks/useSettings.tsx +++ b/app/lib/hooks/useSettings.tsx @@ -3,7 +3,6 @@ import { isDebugMode, isEventLogsEnabled, isLocalModelsEnabled, - isGitHubAuthEnabled, LOCAL_PROVIDERS, promptStore, providersStore, @@ -12,7 +11,7 @@ import { import { useCallback, useEffect, useState } from 'react'; import Cookies from 'js-cookie'; import type { IProviderSetting, ProviderInfo } from '~/types/model'; -import { logStore } from '~/lib/stores/logs'; // assuming logStore is imported from this location +import { logStore } from '~/lib/stores/logs'; import commit from '~/commit.json'; interface CommitData { @@ -29,7 +28,6 @@ export function useSettings() { const promptId = useStore(promptStore); const isLocalModel: boolean = useStore(isLocalModelsEnabled); const isLatestBranch: boolean = useStore(latestBranchStore); - const isGitHubAuth: boolean = useStore(isGitHubAuthEnabled); const [activeProviders, setActiveProviders] = useState([]); // Function to check if we're on stable version @@ -121,13 +119,6 @@ export function useSettings() { } else { latestBranchStore.set(savedLatestBranch === 'true'); } - - // load GitHub authentication from cookies - const savedGitHubAuth = Cookies.get('isGitHubAuthEnabled'); - - if (savedGitHubAuth) { - isGitHubAuthEnabled.set(savedGitHubAuth === 'true'); - } }, []); // writing values to cookies on change @@ -190,20 +181,6 @@ export function useSettings() { Cookies.set('isLatestBranch', String(enabled)); }, []); - const enableGitHubAuth = useCallback((enabled: boolean) => { - isGitHubAuthEnabled.set(enabled); - logStore.logSystem(`GitHub authentication ${enabled ? 'enabled' : 'disabled'}`); - Cookies.set('isGitHubAuthEnabled', String(enabled)); - - // Clean up GitHub data when feature is disabled - if (!enabled) { - localStorage.removeItem('github_token'); - Cookies.remove('githubUsername'); - Cookies.remove('githubToken'); - Cookies.remove('git:github.com'); - } - }, []); - return { providers, activeProviders, @@ -218,7 +195,5 @@ export function useSettings() { setPromptId, isLatestBranch, enableLatestBranch, - isGitHubAuth, - enableGitHubAuth, }; } diff --git a/app/lib/stores/settings.ts b/app/lib/stores/settings.ts index 71fadabe9..cbaf30e95 100644 --- a/app/lib/stores/settings.ts +++ b/app/lib/stores/settings.ts @@ -50,5 +50,3 @@ export const isLocalModelsEnabled = atom(true); export const promptStore = atom('default'); export const latestBranchStore = atom(false); - -export const isGitHubAuthEnabled = atom(false); From d37cf4ecb8460c28aa24b5374da2b3239359da94 Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Fri, 20 Dec 2024 17:03:04 -0500 Subject: [PATCH 25/36] more changes --- app/commit.json | 2 +- app/components/chat/GitCloneButton.tsx | 97 +++++++------- app/components/git/GitCloneModal.tsx | 178 +++++++++++++++++++++++++ app/lib/github/github.client.ts | 32 ++++- 4 files changed, 259 insertions(+), 50 deletions(-) create mode 100644 app/components/git/GitCloneModal.tsx diff --git a/app/commit.json b/app/commit.json index b0e13098c..2be72b492 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "f3f1ea8401cc6d00783cb76fb59805282dbc6347" } +{ "commit": "87579d8370a76abf46ec4c0f540998edb09b08cd" } diff --git a/app/components/chat/GitCloneButton.tsx b/app/components/chat/GitCloneButton.tsx index 4fe4c55e6..ae35145b2 100644 --- a/app/components/chat/GitCloneButton.tsx +++ b/app/components/chat/GitCloneButton.tsx @@ -3,6 +3,8 @@ import { useGit } from '~/lib/hooks/useGit'; import type { Message } from 'ai'; import { detectProjectCommands, createCommandsMessage } from '~/utils/projectCommands'; import { generateId } from '~/utils/fileUtils'; +import { useState } from 'react'; +import { GitCloneModal } from '~/components/git/GitCloneModal'; const IGNORE_PATTERNS = [ 'node_modules/**', @@ -31,47 +33,43 @@ const IGNORE_PATTERNS = [ const ig = ignore().add(IGNORE_PATTERNS); interface GitCloneButtonProps { - className?: string; - importChat?: (description: string, messages: Message[]) => Promise; + importChat: (description: string, messages: Message[]) => Promise; } export default function GitCloneButton({ importChat }: GitCloneButtonProps) { const { ready, gitClone } = useGit(); - const onClick = async (_e: any) => { + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleClone = async (repoUrl: string) => { if (!ready) { return; } - const repoUrl = prompt('Enter the Git url'); - - if (repoUrl) { - const { workdir, data } = await gitClone(repoUrl); - - if (importChat) { - const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath)); - console.log(filePaths); + const { workdir, data } = await gitClone(repoUrl); - const textDecoder = new TextDecoder('utf-8'); + if (importChat) { + const filePaths = Object.keys(data).filter((filePath) => !ig.ignores(filePath)); + const textDecoder = new TextDecoder('utf-8'); - // Convert files to common format for command detection - const fileContents = filePaths - .map((filePath) => { - const { data: content, encoding } = data[filePath]; - return { - path: filePath, - content: encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '', - }; - }) - .filter((f) => f.content); + // Convert files to common format for command detection + const fileContents = filePaths + .map((filePath) => { + const { data: content, encoding } = data[filePath]; + return { + path: filePath, + content: encoding === 'utf8' ? content : content instanceof Uint8Array ? textDecoder.decode(content) : '', + }; + }) + .filter((f) => f.content); - // Detect and create commands message - const commands = await detectProjectCommands(fileContents); - const commandsMessage = createCommandsMessage(commands); + // Detect and create commands message + const commands = await detectProjectCommands(fileContents); + const commandsMessage = createCommandsMessage(commands); - // Create files message - const filesMessage: Message = { - role: 'assistant', - content: `Cloning the repo ${repoUrl} into ${workdir} + // Create files message + const filesMessage: Message = { + role: 'assistant', + content: `Cloning the repo ${repoUrl} into ${workdir} ${fileContents .map( @@ -82,29 +80,36 @@ ${file.content} ) .join('\n')} `, - id: generateId(), - createdAt: new Date(), - }; + id: generateId(), + createdAt: new Date(), + }; - const messages = [filesMessage]; + const messages = [filesMessage]; - if (commandsMessage) { - messages.push(commandsMessage); - } - - await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages); + if (commandsMessage) { + messages.push(commandsMessage); } + + await importChat(`Git Project:${repoUrl.split('/').slice(-1)[0]}`, messages); } }; return ( - + <> + + + setIsModalOpen(false)} + onClone={handleClone} + /> + ); } diff --git a/app/components/git/GitCloneModal.tsx b/app/components/git/GitCloneModal.tsx new file mode 100644 index 000000000..8fc99faa9 --- /dev/null +++ b/app/components/git/GitCloneModal.tsx @@ -0,0 +1,178 @@ +import { useCallback, useEffect, useState } from 'react'; +import { Dialog, DialogRoot } from '~/components/ui/Dialog'; +import { GitHubAuth } from '~/lib/github/GitHubAuth'; +import { getGitHubUser, getUserRepos } from '~/lib/github/github.client'; +import { toast } from 'react-toastify'; + +interface GitCloneModalProps { + open: boolean; + onClose: () => void; + onClone: (url: string) => Promise; +} + +export function GitCloneModal({ open, onClose, onClone }: GitCloneModalProps) { + const [publicUrl, setPublicUrl] = useState(''); + const [userRepos, setUserRepos] = useState>([]); + const [selectedRepo, setSelectedRepo] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [username, setUsername] = useState(''); + + const loadUserRepos = useCallback(async () => { + const token = localStorage.getItem('github_token'); + if (!token) { + setIsAuthenticated(false); + return; + } + + try { + setIsLoading(true); + const user = await getGitHubUser(token); + setUsername(user.login); + const repos = await getUserRepos(token); + setUserRepos(repos.map(repo => ({ + name: repo.full_name, + url: repo.clone_url + }))); + setIsAuthenticated(true); + } catch (error) { + console.error('Error loading repos:', error); + toast.error('Failed to load repositories'); + if (error instanceof Error && 'status' in error && (error.status === 401 || error.status === 403)) { + localStorage.removeItem('github_token'); + setIsAuthenticated(false); + } + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + if (open) { + loadUserRepos(); + } + }, [open, loadUserRepos]); + + const handleAuthComplete = useCallback(async (token: string) => { + try { + const user = await getGitHubUser(token); + setUsername(user.login); + setIsAuthenticated(true); + loadUserRepos(); + } catch (error) { + console.error('Auth error:', error); + toast.error('Authentication failed'); + localStorage.removeItem('github_token'); + setIsAuthenticated(false); + } + }, [loadUserRepos]); + + const handleClone = useCallback(async () => { + try { + if (selectedRepo) { + await onClone(selectedRepo); + } else if (publicUrl) { + await onClone(publicUrl); + } + onClose(); + } catch (error) { + console.error('Clone error:', error); + toast.error('Failed to clone repository'); + } + }, [selectedRepo, publicUrl, onClone, onClose]); + + return ( + + +
+

Clone Repository

+ +
+ +
+ {(!selectedRepo || !isAuthenticated) && ( +
+
Public Repository
+ { + setPublicUrl(e.target.value); + if (e.target.value && selectedRepo) { + setSelectedRepo(''); + } + }} + className="w-full p-2 h-[32px] rounded bg-[#2D2D2D] border border-[#383838] text-white placeholder-[#8B8B8B] focus:outline-none focus:border-[#525252]" + /> +
+ )} + +
+
+ {isAuthenticated ? `${username}'s Repositories` : 'Private Repository'} +
+ {isAuthenticated ? ( + + ) : ( + toast.error(error.message)}> + + + )} + {isLoading && ( +
+
+
+ )} +
+ +
+ + +
+
+
+
+ ); +} diff --git a/app/lib/github/github.client.ts b/app/lib/github/github.client.ts index 7e4f1a92a..feeb3438b 100644 --- a/app/lib/github/github.client.ts +++ b/app/lib/github/github.client.ts @@ -6,7 +6,16 @@ export interface GitHubUser { email?: string; } -interface GitHubRepo { +export interface GitHubRepo { + id: number; + name: string; + full_name: string; + private: boolean; + clone_url: string; + html_url: string; +} + +interface GitHubRepoCreate { name: string; owner: { login: string; @@ -41,9 +50,26 @@ export async function getGitHubUser(token: string): Promise { return githubRequest('/user', token); } -export async function createRepository(token: string, name: string, description?: string): Promise { +export async function getUserRepos(token: string): Promise { + const response = await fetch('https://api.github.com/user/repos?sort=updated&per_page=100', { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github.v3+json', + }, + }); + + if (!response.ok) { + const error = new Error('Failed to fetch user repositories') as any; + error.status = response.status; + throw error; + } + + return response.json(); +} + +export async function createRepository(token: string, name: string, description?: string): Promise { // First create the repository without auto_init - const repo = await githubRequest('/user/repos', token, 'POST', { + const repo = await githubRequest('/user/repos', token, 'POST', { name, description: description || 'Created with Bolt', private: false, From 18d72e9fc93307d3c5ebcbfbb1361967fa28e961 Mon Sep 17 00:00:00 2001 From: Ed McConnell Date: Fri, 20 Dec 2024 17:03:28 -0500 Subject: [PATCH 26/36] more changes --- app/commit.json | 2 +- app/components/chat/BaseChat.tsx | 2 +- app/components/chat/GitCloneButton.tsx | 8 +--- app/components/git/GitCloneModal.tsx | 56 ++++++++++++++++---------- 4 files changed, 39 insertions(+), 29 deletions(-) diff --git a/app/commit.json b/app/commit.json index 2be72b492..cb35a07f6 100644 --- a/app/commit.json +++ b/app/commit.json @@ -1 +1 @@ -{ "commit": "87579d8370a76abf46ec4c0f540998edb09b08cd" } +{ "commit": "d37cf4ecb8460c28aa24b5374da2b3239359da94" } diff --git a/app/components/chat/BaseChat.tsx b/app/components/chat/BaseChat.tsx index 5db66537c..afa49a263 100644 --- a/app/components/chat/BaseChat.tsx +++ b/app/components/chat/BaseChat.tsx @@ -544,7 +544,7 @@ export const BaseChat = React.forwardRef( {!chatStarted && (
{ImportButtons(importChat)} - + {importChat && }
)} {!chatStarted && diff --git a/app/components/chat/GitCloneButton.tsx b/app/components/chat/GitCloneButton.tsx index ae35145b2..2313bf868 100644 --- a/app/components/chat/GitCloneButton.tsx +++ b/app/components/chat/GitCloneButton.tsx @@ -33,7 +33,7 @@ const IGNORE_PATTERNS = [ const ig = ignore().add(IGNORE_PATTERNS); interface GitCloneButtonProps { - importChat: (description: string, messages: Message[]) => Promise; + importChat?: (description: string, messages: Message[]) => Promise; } export default function GitCloneButton({ importChat }: GitCloneButtonProps) { @@ -105,11 +105,7 @@ ${file.content} Clone a Git Repo - setIsModalOpen(false)} - onClone={handleClone} - /> + setIsModalOpen(false)} onClone={handleClone} /> ); } diff --git a/app/components/git/GitCloneModal.tsx b/app/components/git/GitCloneModal.tsx index 8fc99faa9..0b98f59b5 100644 --- a/app/components/git/GitCloneModal.tsx +++ b/app/components/git/GitCloneModal.tsx @@ -20,6 +20,7 @@ export function GitCloneModal({ open, onClose, onClone }: GitCloneModalProps) { const loadUserRepos = useCallback(async () => { const token = localStorage.getItem('github_token'); + if (!token) { setIsAuthenticated(false); return; @@ -27,17 +28,22 @@ export function GitCloneModal({ open, onClose, onClone }: GitCloneModalProps) { try { setIsLoading(true); + const user = await getGitHubUser(token); setUsername(user.login); + const repos = await getUserRepos(token); - setUserRepos(repos.map(repo => ({ - name: repo.full_name, - url: repo.clone_url - }))); + setUserRepos( + repos.map((repo) => ({ + name: repo.full_name, + url: repo.clone_url, + })), + ); setIsAuthenticated(true); } catch (error) { console.error('Error loading repos:', error); toast.error('Failed to load repositories'); + if (error instanceof Error && 'status' in error && (error.status === 401 || error.status === 403)) { localStorage.removeItem('github_token'); setIsAuthenticated(false); @@ -53,19 +59,22 @@ export function GitCloneModal({ open, onClose, onClone }: GitCloneModalProps) { } }, [open, loadUserRepos]); - const handleAuthComplete = useCallback(async (token: string) => { - try { - const user = await getGitHubUser(token); - setUsername(user.login); - setIsAuthenticated(true); - loadUserRepos(); - } catch (error) { - console.error('Auth error:', error); - toast.error('Authentication failed'); - localStorage.removeItem('github_token'); - setIsAuthenticated(false); - } - }, [loadUserRepos]); + const handleAuthComplete = useCallback( + async (token: string) => { + try { + const user = await getGitHubUser(token); + setUsername(user.login); + setIsAuthenticated(true); + loadUserRepos(); + } catch (error) { + console.error('Auth error:', error); + toast.error('Authentication failed'); + localStorage.removeItem('github_token'); + setIsAuthenticated(false); + } + }, + [loadUserRepos], + ); const handleClone = useCallback(async () => { try { @@ -74,6 +83,7 @@ export function GitCloneModal({ open, onClose, onClone }: GitCloneModalProps) { } else if (publicUrl) { await onClone(publicUrl); } + onClose(); } catch (error) { console.error('Clone error:', error); @@ -101,6 +111,7 @@ export function GitCloneModal({ open, onClose, onClone }: GitCloneModalProps) { value={publicUrl} onChange={(e) => { setPublicUrl(e.target.value); + if (e.target.value && selectedRepo) { setSelectedRepo(''); } @@ -119,20 +130,23 @@ export function GitCloneModal({ open, onClose, onClone }: GitCloneModalProps) { value={selectedRepo} onChange={(e) => { setSelectedRepo(e.target.value); + if (e.target.value) { setPublicUrl(''); } }} className="w-full px-2 h-9 rounded bg-[#2D2D2D] border border-[#383838] text-white focus:outline-none focus:border-[#525252] text-ellipsis appearance-none" - style={{ - textOverflow: 'ellipsis', + style={{ + textOverflow: 'ellipsis', whiteSpace: 'nowrap', lineHeight: '2rem', paddingTop: '0', - paddingBottom: '0' + paddingBottom: '0', }} > - + {userRepos.map((repo) => (