From b01874205ebb294d88a2db57b9bad437ba4ee299 Mon Sep 17 00:00:00 2001 From: KevIsDev Date: Mon, 3 Mar 2025 09:24:39 +0000 Subject: [PATCH 01/13] fix: support php language in diff view --- app/components/workbench/DiffView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/workbench/DiffView.tsx b/app/components/workbench/DiffView.tsx index aa635ba6..73a5a339 100644 --- a/app/components/workbench/DiffView.tsx +++ b/app/components/workbench/DiffView.tsx @@ -556,7 +556,7 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language } useEffect(() => { getHighlighter({ themes: ['github-dark', 'github-light'], - langs: ['typescript', 'javascript', 'json', 'html', 'css', 'jsx', 'tsx'], + langs: ['typescript', 'javascript', 'json', 'html', 'css', 'jsx', 'tsx', 'python', 'php'], }).then(setHighlighter); }, []); From 964e1973fbb6b211ce200f408af469eaace7f132 Mon Sep 17 00:00:00 2001 From: KevIsDev Date: Mon, 3 Mar 2025 09:39:16 +0000 Subject: [PATCH 02/13] fix: added a bunch more common languages to diff view including: java, c, cpp, csharp, go ruby, rust --- app/components/workbench/DiffView.tsx | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/components/workbench/DiffView.tsx b/app/components/workbench/DiffView.tsx index 73a5a339..146076c5 100644 --- a/app/components/workbench/DiffView.tsx +++ b/app/components/workbench/DiffView.tsx @@ -556,7 +556,25 @@ const InlineDiffComparison = memo(({ beforeCode, afterCode, filename, language } useEffect(() => { getHighlighter({ themes: ['github-dark', 'github-light'], - langs: ['typescript', 'javascript', 'json', 'html', 'css', 'jsx', 'tsx', 'python', 'php'], + langs: [ + 'typescript', + 'javascript', + 'json', + 'html', + 'css', + 'jsx', + 'tsx', + 'python', + 'php', + 'java', + 'c', + 'cpp', + 'csharp', + 'go', + 'ruby', + 'rust', + 'plaintext', + ], }).then(setHighlighter); }, []); From 60b6f476d7515c34a1aafe65a55f6661e00ff8cc Mon Sep 17 00:00:00 2001 From: Leex Date: Mon, 3 Mar 2025 15:57:25 +0100 Subject: [PATCH 03/13] Delete wrangler.toml --- wrangler.toml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 wrangler.toml diff --git a/wrangler.toml b/wrangler.toml deleted file mode 100644 index addd1007..00000000 --- a/wrangler.toml +++ /dev/null @@ -1,6 +0,0 @@ -#:schema node_modules/wrangler/config-schema.json -name = "bolt" -compatibility_flags = ["nodejs_compat"] -compatibility_date = "2024-07-01" -pages_build_output_dir = "./build/client" -send_metrics = false \ No newline at end of file From 2780b2ebe1226ff6f88df591e34d049d257653fe Mon Sep 17 00:00:00 2001 From: Leex Date: Mon, 3 Mar 2025 15:57:41 +0100 Subject: [PATCH 04/13] Delete .tool-versions --- .tool-versions | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .tool-versions diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index 74c88f6a..00000000 --- a/.tool-versions +++ /dev/null @@ -1,2 +0,0 @@ -nodejs 20.15.1 -pnpm 9.4.0 \ No newline at end of file From 8d1f1382243e801ae1b0fcfca7eac6f8ebb0cf0c Mon Sep 17 00:00:00 2001 From: Anirban Kar Date: Mon, 3 Mar 2025 21:21:04 +0530 Subject: [PATCH 05/13] Revert "Delete wrangler.toml" This reverts commit 60b6f476d7515c34a1aafe65a55f6661e00ff8cc. --- wrangler.toml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 wrangler.toml diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 00000000..addd1007 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,6 @@ +#:schema node_modules/wrangler/config-schema.json +name = "bolt" +compatibility_flags = ["nodejs_compat"] +compatibility_date = "2024-07-01" +pages_build_output_dir = "./build/client" +send_metrics = false \ No newline at end of file From 9b2a204ddc3ff0cb75236fe4e56ee103e32dcbb2 Mon Sep 17 00:00:00 2001 From: Anirban Kar Date: Tue, 4 Mar 2025 20:28:51 +0530 Subject: [PATCH 06/13] ci: added arm64 build and tags build --- .github/workflows/docker.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 42070f9f..b44e63a0 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -5,6 +5,9 @@ on: branches: - main - stable + tags: + - 'v*' + - '*.*.*' workflow_dispatch: permissions: @@ -21,6 +24,10 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -37,12 +44,17 @@ jobs: uses: docker/metadata-action@v4 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha - name: Build and push Docker image for main if: github.ref == 'refs/heads/main' uses: docker/build-push-action@v6 with: context: . + platforms: linux/amd64,linux/arm64 push: true tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest @@ -54,8 +66,20 @@ jobs: uses: docker/build-push-action@v6 with: context: . + platforms: linux/amd64,linux/arm64 push: true tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:stable ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} labels: ${{ steps.meta.outputs.labels }} + + - name: Build and push Docker image for tags + if: startsWith(github.ref, 'refs/tags/') + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + labels: ${{ steps.meta.outputs.labels }} From 2452f9413d835508df3e6ea50b57bcbccd0645c8 Mon Sep 17 00:00:00 2001 From: Anirban Kar Date: Tue, 4 Mar 2025 20:37:33 +0530 Subject: [PATCH 07/13] ci: updated to have concise and parallel builds --- .github/workflows/docker.yaml | 58 ++++++++++------------------------- 1 file changed, 17 insertions(+), 41 deletions(-) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index b44e63a0..05ab7831 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -2,14 +2,14 @@ name: Docker Publish on: push: - branches: - - main - - stable - tags: - - 'v*' - - '*.*.*' + branches: [main, stable] + tags: ['v*', '*.*.*'] workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + permissions: packages: write contents: read @@ -21,17 +21,14 @@ env: jobs: docker-build-publish: runs-on: ubuntu-latest + # timeout-minutes: 30 steps: - name: Checkout code uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - + - name: Log in to GitHub Container Registry uses: docker/login-action@v3 with: @@ -45,41 +42,20 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | - type=ref,event=branch + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + type=raw,value=stable,enable=${{ github.ref == 'refs/heads/stable' }} type=ref,event=tag - type=sha + type=sha,format=short + type=raw,value=${{ github.ref_name }},enable=${{ startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/stable' }} - - name: Build and push Docker image for main - if: github.ref == 'refs/heads/main' + - name: Build and push Docker image uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64 push: true - tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Build and push Docker image for stable - if: github.ref == 'refs/heads/stable' - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:stable - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} - labels: ${{ steps.meta.outputs.labels }} - - - name: Build and push Docker image for tags - if: startsWith(github.ref, 'refs/tags/') - uses: docker/build-push-action@v6 - with: - context: . - platforms: linux/amd64,linux/arm64 - push: true - tags: | - ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + + - name: Check manifest + run: docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }} \ No newline at end of file From f9436d4929bbb41a316dee69472145d8b71cca0a Mon Sep 17 00:00:00 2001 From: Anirban Kar Date: Wed, 5 Mar 2025 03:58:01 +0530 Subject: [PATCH 08/13] ci: updated target for docker build (#1451) --- .github/workflows/docker.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml index 05ab7831..a038e02f 100644 --- a/.github/workflows/docker.yaml +++ b/.github/workflows/docker.yaml @@ -53,6 +53,7 @@ jobs: with: context: . platforms: linux/amd64,linux/arm64 + target: bolt-ai-production push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From 73a0f3ae24c3dc79c68d075b49dea3883d4ce694 Mon Sep 17 00:00:00 2001 From: Anirban Kar Date: Wed, 5 Mar 2025 04:23:01 +0530 Subject: [PATCH 09/13] fix: git clone modal to work with non main as default branch (#1428) --- .../components/RepositorySelectionDialog.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/app/components/@settings/tabs/connections/components/RepositorySelectionDialog.tsx b/app/components/@settings/tabs/connections/components/RepositorySelectionDialog.tsx index 06202850..5f07e727 100644 --- a/app/components/@settings/tabs/connections/components/RepositorySelectionDialog.tsx +++ b/app/components/@settings/tabs/connections/components/RepositorySelectionDialog.tsx @@ -292,11 +292,24 @@ export function RepositorySelectionDialog({ isOpen, onClose, onSelect }: Reposit const connection = getLocalStorage('github_connection'); const headers: HeadersInit = connection?.token ? { Authorization: `Bearer ${connection.token}` } : {}; - - // Fetch repository tree - const treeResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/main?recursive=1`, { + const repoObjResponse = await fetch(`https://api.github.com/repos/${owner}/${repo}`, { headers, }); + const repoObjData = (await repoObjResponse.json()) as any; + + if (!repoObjData.default_branch) { + throw new Error('Failed to fetch repository branch'); + } + + const defaultBranch = repoObjData.default_branch; + + // Fetch repository tree + const treeResponse = await fetch( + `https://api.github.com/repos/${owner}/${repo}/git/trees/${defaultBranch}?recursive=1`, + { + headers, + }, + ); if (!treeResponse.ok) { throw new Error('Failed to fetch repository structure'); From 1f940391b19e7187890623150136d24f0adb6322 Mon Sep 17 00:00:00 2001 From: Anirban Kar Date: Wed, 5 Mar 2025 10:59:48 +0530 Subject: [PATCH 10/13] feat: restoring project from snapshot on reload (#444) * feat:(project-snapshot) restoring project from snapshot on reload * minor bugfix * updated message * added snapshot reload with auto run dev commands * added message context * snapshot updated --- app/lib/persistence/types.ts | 7 + app/lib/persistence/useChatHistory.ts | 218 +++++++++++++++++++++++++- 2 files changed, 217 insertions(+), 8 deletions(-) create mode 100644 app/lib/persistence/types.ts diff --git a/app/lib/persistence/types.ts b/app/lib/persistence/types.ts new file mode 100644 index 00000000..56dacd61 --- /dev/null +++ b/app/lib/persistence/types.ts @@ -0,0 +1,7 @@ +import type { FileMap } from '~/lib/stores/files'; + +export interface Snapshot { + chatIndex: string; + files: FileMap; + summary?: string; +} diff --git a/app/lib/persistence/useChatHistory.ts b/app/lib/persistence/useChatHistory.ts index 7baefa56..b8b5c833 100644 --- a/app/lib/persistence/useChatHistory.ts +++ b/app/lib/persistence/useChatHistory.ts @@ -1,7 +1,7 @@ import { useLoaderData, useNavigate, useSearchParams } from '@remix-run/react'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { atom } from 'nanostores'; -import type { Message } from 'ai'; +import { generateId, type JSONValue, type Message } from 'ai'; import { toast } from 'react-toastify'; import { workbenchStore } from '~/lib/stores/workbench'; import { logStore } from '~/lib/stores/logs'; // Import logStore @@ -15,6 +15,11 @@ import { createChatFromMessages, type IChatMetadata, } from './db'; +import type { FileMap } from '~/lib/stores/files'; +import type { Snapshot } from './types'; +import { webcontainer } from '~/lib/webcontainer'; +import { createCommandsMessage, detectProjectCommands } from '~/utils/projectCommands'; +import type { ContextAnnotation } from '~/types/context'; export interface ChatHistoryItem { id: string; @@ -37,6 +42,7 @@ export function useChatHistory() { const { id: mixedId } = useLoaderData<{ id?: string }>(); const [searchParams] = useSearchParams(); + const [archivedMessages, setArchivedMessages] = useState([]); const [initialMessages, setInitialMessages] = useState([]); const [ready, setReady] = useState(false); const [urlId, setUrlId] = useState(); @@ -56,14 +62,128 @@ export function useChatHistory() { if (mixedId) { getMessages(db, mixedId) - .then((storedMessages) => { + .then(async (storedMessages) => { if (storedMessages && storedMessages.messages.length > 0) { + const snapshotStr = localStorage.getItem(`snapshot:${mixedId}`); + const snapshot: Snapshot = snapshotStr ? JSON.parse(snapshotStr) : { chatIndex: 0, files: {} }; + const summary = snapshot.summary; + const rewindId = searchParams.get('rewindTo'); - const filteredMessages = rewindId - ? storedMessages.messages.slice(0, storedMessages.messages.findIndex((m) => m.id === rewindId) + 1) - : storedMessages.messages; + let startingIdx = 0; + const endingIdx = rewindId + ? storedMessages.messages.findIndex((m) => m.id === rewindId) + 1 + : storedMessages.messages.length; + const snapshotIndex = storedMessages.messages.findIndex((m) => m.id === snapshot.chatIndex); + + if (snapshotIndex >= 0 && snapshotIndex < endingIdx) { + startingIdx = snapshotIndex; + } + + if (snapshotIndex > 0 && storedMessages.messages[snapshotIndex].id == rewindId) { + startingIdx = 0; + } + + let filteredMessages = storedMessages.messages.slice(startingIdx + 1, endingIdx); + let archivedMessages: Message[] = []; + + if (startingIdx > 0) { + archivedMessages = storedMessages.messages.slice(0, startingIdx + 1); + } + + setArchivedMessages(archivedMessages); + + if (startingIdx > 0) { + const files = Object.entries(snapshot?.files || {}) + .map(([key, value]) => { + if (value?.type !== 'file') { + return null; + } + + return { + content: value.content, + path: key, + }; + }) + .filter((x) => !!x); + const projectCommands = await detectProjectCommands(files); + const commands = createCommandsMessage(projectCommands); + + filteredMessages = [ + { + id: generateId(), + role: 'user', + content: `Restore project from snapshot + `, + annotations: ['no-store', 'hidden'], + }, + { + id: storedMessages.messages[snapshotIndex].id, + role: 'assistant', + content: ` 📦 Chat Restored from snapshot, You can revert this message to load the full chat history + + ${Object.entries(snapshot?.files || {}) + .filter((x) => !x[0].endsWith('lock.json')) + .map(([key, value]) => { + if (value?.type === 'file') { + return ` + +${value.content} + + `; + } else { + return ``; + } + }) + .join('\n')} + + `, + annotations: [ + 'no-store', + ...(summary + ? [ + { + chatId: storedMessages.messages[snapshotIndex].id, + type: 'chatSummary', + summary, + } satisfies ContextAnnotation, + ] + : []), + ], + }, + ...(commands !== null + ? [ + { + id: `${storedMessages.messages[snapshotIndex].id}-2`, + role: 'user' as const, + content: `setup project`, + annotations: ['no-store', 'hidden'], + }, + { + ...commands, + id: `${storedMessages.messages[snapshotIndex].id}-3`, + annotations: [ + 'no-store', + ...(commands.annotations || []), + ...(summary + ? [ + { + chatId: `${storedMessages.messages[snapshotIndex].id}-3`, + type: 'chatSummary', + summary, + } satisfies ContextAnnotation, + ] + : []), + ], + }, + ] + : []), + ...filteredMessages, + ]; + restoreSnapshot(mixedId); + } setInitialMessages(filteredMessages); + setUrlId(storedMessages.urlId); description.set(storedMessages.description); chatId.set(storedMessages.id); @@ -75,10 +195,64 @@ export function useChatHistory() { setReady(true); }) .catch((error) => { + console.error(error); + logStore.logError('Failed to load chat messages', error); toast.error(error.message); }); } + }, [mixedId]); + + const takeSnapshot = useCallback( + async (chatIdx: string, files: FileMap, _chatId?: string | undefined, chatSummary?: string) => { + const id = _chatId || chatId; + + if (!id) { + return; + } + + const snapshot: Snapshot = { + chatIndex: chatIdx, + files, + summary: chatSummary, + }; + localStorage.setItem(`snapshot:${id}`, JSON.stringify(snapshot)); + }, + [chatId], + ); + + const restoreSnapshot = useCallback(async (id: string) => { + const snapshotStr = localStorage.getItem(`snapshot:${id}`); + const container = await webcontainer; + + // if (snapshotStr)setSnapshot(JSON.parse(snapshotStr)); + const snapshot: Snapshot = snapshotStr ? JSON.parse(snapshotStr) : { chatIndex: 0, files: {} }; + + if (!snapshot?.files) { + return; + } + + Object.entries(snapshot.files).forEach(async ([key, value]) => { + if (key.startsWith(container.workdir)) { + key = key.replace(container.workdir, ''); + } + + if (value?.type === 'folder') { + await container.fs.mkdir(key, { recursive: true }); + } + }); + Object.entries(snapshot.files).forEach(async ([key, value]) => { + if (value?.type === 'file') { + if (key.startsWith(container.workdir)) { + key = key.replace(container.workdir, ''); + } + + await container.fs.writeFile(key, value.content, { encoding: value.isBinary ? undefined : 'utf8' }); + } else { + } + }); + + // workbenchStore.files.setKey(snapshot?.files) }, []); return { @@ -105,14 +279,34 @@ export function useChatHistory() { } const { firstArtifact } = workbenchStore; + messages = messages.filter((m) => !m.annotations?.includes('no-store')); + + let _urlId = urlId; if (!urlId && firstArtifact?.id) { const urlId = await getUrlId(db, firstArtifact.id); - + _urlId = urlId; navigateChat(urlId); setUrlId(urlId); } + let chatSummary: string | undefined = undefined; + const lastMessage = messages[messages.length - 1]; + + if (lastMessage.role === 'assistant') { + const annotations = lastMessage.annotations as JSONValue[]; + const filteredAnnotations = (annotations?.filter( + (annotation: JSONValue) => + annotation && typeof annotation === 'object' && Object.keys(annotation).includes('type'), + ) || []) as { type: string; value: any } & { [key: string]: any }[]; + + if (filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')) { + chatSummary = filteredAnnotations.find((annotation) => annotation.type === 'chatSummary')?.summary; + } + } + + takeSnapshot(messages[messages.length - 1].id, workbenchStore.files.get(), _urlId, chatSummary); + if (!description.get() && firstArtifact?.title) { description.set(firstArtifact?.title); } @@ -127,7 +321,15 @@ export function useChatHistory() { } } - await setMessages(db, chatId.get() as string, messages, urlId, description.get(), undefined, chatMetadata.get()); + await setMessages( + db, + chatId.get() as string, + [...archivedMessages, ...messages], + urlId, + description.get(), + undefined, + chatMetadata.get(), + ); }, duplicateCurrentChat: async (listItemId: string) => { if (!db || (!mixedId && !listItemId)) { From 20722a108ca619c7eca68b063ae6c7af786d4efe Mon Sep 17 00:00:00 2001 From: Burhanuddin Khatri <144617735+BurhanCantCode@users.noreply.github.com> Date: Wed, 5 Mar 2025 18:42:52 +0500 Subject: [PATCH 11/13] feat: add Claude 3.7 Sonnet model as static list and update API key reference (#1449) --- app/lib/modules/llm/providers/anthropic.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/lib/modules/llm/providers/anthropic.ts b/app/lib/modules/llm/providers/anthropic.ts index f8dca5a8..70f93c07 100644 --- a/app/lib/modules/llm/providers/anthropic.ts +++ b/app/lib/modules/llm/providers/anthropic.ts @@ -13,6 +13,12 @@ export default class AnthropicProvider extends BaseProvider { }; staticModels: ModelInfo[] = [ + { + name: 'claude-3-7-sonnet-20250219', + label: 'Claude 3.7 Sonnet', + provider: 'Anthropic', + maxTokenAllowed: 8000, + }, { name: 'claude-3-5-sonnet-latest', label: 'Claude 3.5 Sonnet (new)', @@ -46,7 +52,7 @@ export default class AnthropicProvider extends BaseProvider { providerSettings: settings, serverEnv: serverEnv as any, defaultBaseUrlKey: '', - defaultApiTokenKey: 'OPENAI_API_KEY', + defaultApiTokenKey: 'ANTHROPIC_API_KEY', }); if (!apiKey) { From 9780393b17de28c9bad6fecee4a5ce713541eb0f Mon Sep 17 00:00:00 2001 From: Anirban Kar Date: Fri, 7 Mar 2025 00:29:44 +0530 Subject: [PATCH 12/13] fix: git cookies are auto set anytime connects changed or loaded (#1461) --- .../tabs/connections/GithubConnection.tsx | 94 +++++++++++-------- 1 file changed, 54 insertions(+), 40 deletions(-) diff --git a/app/components/@settings/tabs/connections/GithubConnection.tsx b/app/components/@settings/tabs/connections/GithubConnection.tsx index 9f433724..36197f2e 100644 --- a/app/components/@settings/tabs/connections/GithubConnection.tsx +++ b/app/components/@settings/tabs/connections/GithubConnection.tsx @@ -77,6 +77,46 @@ export function GithubConnection() { const [isFetchingStats, setIsFetchingStats] = useState(false); const [isStatsExpanded, setIsStatsExpanded] = useState(false); + const fetchGithubUser = async (token: string) => { + try { + setIsConnecting(true); + + const response = await fetch('https://api.github.com/user', { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error('Invalid token or unauthorized'); + } + + const data = (await response.json()) as GitHubUserResponse; + const newConnection: GitHubConnection = { + user: data, + token, + tokenType: connection.tokenType, + }; + + localStorage.setItem('github_connection', JSON.stringify(newConnection)); + Cookies.set('githubToken', token); + Cookies.set('githubUsername', data.login); + Cookies.set('git:github.com', JSON.stringify({ username: token, password: 'x-oauth-basic' })); + + setConnection(newConnection); + + await fetchGitHubStats(token); + + toast.success('Successfully connected to GitHub'); + } catch (error) { + logStore.logError('Failed to authenticate with GitHub', { error }); + toast.error('Failed to connect to GitHub'); + setConnection({ user: null, token: '', tokenType: 'classic' }); + } finally { + setIsConnecting(false); + } + }; + const fetchGitHubStats = async (token: string) => { try { setIsFetchingStats(true); @@ -182,51 +222,25 @@ export function GithubConnection() { setIsLoading(false); }, []); + useEffect(() => { + if (!connection) { + return; + } + + const token = connection.token; + const data = connection.user; + Cookies.set('githubToken', token); + Cookies.set('git:github.com', JSON.stringify({ username: token, password: 'x-oauth-basic' })); + + if (data) { + Cookies.set('githubUsername', data.login); + } + }, [connection]); if (isLoading || isConnecting || isFetchingStats) { return ; } - const fetchGithubUser = async (token: string) => { - try { - setIsConnecting(true); - - const response = await fetch('https://api.github.com/user', { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (!response.ok) { - throw new Error('Invalid token or unauthorized'); - } - - const data = (await response.json()) as GitHubUserResponse; - const newConnection: GitHubConnection = { - user: data, - token, - tokenType: connection.tokenType, - }; - - localStorage.setItem('github_connection', JSON.stringify(newConnection)); - Cookies.set('githubToken', token); - Cookies.set('githubUsername', data.login); - Cookies.set('git:github.com', JSON.stringify({ username: token, password: 'x-oauth-basic' })); - - setConnection(newConnection); - - await fetchGitHubStats(token); - - toast.success('Successfully connected to GitHub'); - } catch (error) { - logStore.logError('Failed to authenticate with GitHub', { error }); - toast.error('Failed to connect to GitHub'); - setConnection({ user: null, token: '', tokenType: 'classic' }); - } finally { - setIsConnecting(false); - } - }; - const handleConnect = async (event: React.FormEvent) => { event.preventDefault(); await fetchGithubUser(connection.token); From cd4a5e83809759efb82ed0e26abc7a9349ae47ff Mon Sep 17 00:00:00 2001 From: Anirban Kar Date: Fri, 7 Mar 2025 14:10:37 +0530 Subject: [PATCH 13/13] fix: fix git proxy to work with other git provider (#1466) --- app/lib/persistence/useChatHistory.ts | 6 +- app/routes/api.git-proxy.$.ts | 176 +++++++++++++++++++++----- 2 files changed, 145 insertions(+), 37 deletions(-) diff --git a/app/lib/persistence/useChatHistory.ts b/app/lib/persistence/useChatHistory.ts index b8b5c833..3077ca44 100644 --- a/app/lib/persistence/useChatHistory.ts +++ b/app/lib/persistence/useChatHistory.ts @@ -69,7 +69,7 @@ export function useChatHistory() { const summary = snapshot.summary; const rewindId = searchParams.get('rewindTo'); - let startingIdx = 0; + let startingIdx = -1; const endingIdx = rewindId ? storedMessages.messages.findIndex((m) => m.id === rewindId) + 1 : storedMessages.messages.length; @@ -80,13 +80,13 @@ export function useChatHistory() { } if (snapshotIndex > 0 && storedMessages.messages[snapshotIndex].id == rewindId) { - startingIdx = 0; + startingIdx = -1; } let filteredMessages = storedMessages.messages.slice(startingIdx + 1, endingIdx); let archivedMessages: Message[] = []; - if (startingIdx > 0) { + if (startingIdx >= 0) { archivedMessages = storedMessages.messages.slice(0, startingIdx + 1); } diff --git a/app/routes/api.git-proxy.$.ts b/app/routes/api.git-proxy.$.ts index 9e6cb3b1..45230520 100644 --- a/app/routes/api.git-proxy.$.ts +++ b/app/routes/api.git-proxy.$.ts @@ -1,6 +1,47 @@ import { json } from '@remix-run/cloudflare'; import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/cloudflare'; +// Allowed headers to forward to the target server +const ALLOW_HEADERS = [ + 'accept-encoding', + 'accept-language', + 'accept', + 'access-control-allow-origin', + 'authorization', + 'cache-control', + 'connection', + 'content-length', + 'content-type', + 'dnt', + 'pragma', + 'range', + 'referer', + 'user-agent', + 'x-authorization', + 'x-http-method-override', + 'x-requested-with', +]; + +// Headers to expose from the target server's response +const EXPOSE_HEADERS = [ + 'accept-ranges', + 'age', + 'cache-control', + 'content-length', + 'content-language', + 'content-type', + 'date', + 'etag', + 'expires', + 'last-modified', + 'pragma', + 'server', + 'transfer-encoding', + 'vary', + 'x-github-request-id', + 'x-redirected-url', +]; + // Handle all HTTP methods export async function action({ request, params }: ActionFunctionArgs) { return handleProxyRequest(request, params['*']); @@ -16,50 +57,117 @@ async function handleProxyRequest(request: Request, path: string | undefined) { return json({ error: 'Invalid proxy URL format' }, { status: 400 }); } - const url = new URL(request.url); - - // Reconstruct the target URL - const targetURL = `https://${path}${url.search}`; - - // Forward the request to the target URL - const response = await fetch(targetURL, { - method: request.method, - headers: { - ...Object.fromEntries(request.headers), - - // Override host header with the target host - host: new URL(targetURL).host, - }, - body: ['GET', 'HEAD'].includes(request.method) ? null : await request.arrayBuffer(), - }); - - // Create response with CORS headers - const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', - 'Access-Control-Allow-Headers': '*', - }; - - // Handle preflight requests + // Handle CORS preflight request if (request.method === 'OPTIONS') { return new Response(null, { - headers: corsHeaders, - status: 204, + status: 200, + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'POST, GET, OPTIONS', + 'Access-Control-Allow-Headers': ALLOW_HEADERS.join(', '), + 'Access-Control-Expose-Headers': EXPOSE_HEADERS.join(', '), + 'Access-Control-Max-Age': '86400', + }, }); } - // Forward the response with CORS headers - const responseHeaders = new Headers(response.headers); - Object.entries(corsHeaders).forEach(([key, value]) => { - responseHeaders.set(key, value); - }); + // Extract domain and remaining path + const parts = path.match(/([^\/]+)\/?(.*)/); + if (!parts) { + return json({ error: 'Invalid path format' }, { status: 400 }); + } + + const domain = parts[1]; + const remainingPath = parts[2] || ''; + + // Reconstruct the target URL with query parameters + const url = new URL(request.url); + const targetURL = `https://${domain}/${remainingPath}${url.search}`; + + console.log('Target URL:', targetURL); + + // Filter and prepare headers + const headers = new Headers(); + + // Only forward allowed headers + for (const header of ALLOW_HEADERS) { + if (request.headers.has(header)) { + headers.set(header, request.headers.get(header)!); + } + } + + // Set the host header + headers.set('Host', domain); + + // Set Git user agent if not already present + if (!headers.has('user-agent') || !headers.get('user-agent')?.startsWith('git/')) { + headers.set('User-Agent', 'git/@isomorphic-git/cors-proxy'); + } + + console.log('Request headers:', Object.fromEntries(headers.entries())); + + // Prepare fetch options + const fetchOptions: RequestInit = { + method: request.method, + headers, + redirect: 'follow', + }; + + // Add body and duplex option for non-GET/HEAD requests + if (!['GET', 'HEAD'].includes(request.method)) { + fetchOptions.body = request.body; + fetchOptions.duplex = 'half'; // This fixes the "duplex option is required when sending a body" error + } + + // Forward the request to the target URL + const response = await fetch(targetURL, fetchOptions); + + console.log('Response status:', response.status); + + // Create response headers + const responseHeaders = new Headers(); + + // Add CORS headers + responseHeaders.set('Access-Control-Allow-Origin', '*'); + responseHeaders.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS'); + responseHeaders.set('Access-Control-Allow-Headers', ALLOW_HEADERS.join(', ')); + responseHeaders.set('Access-Control-Expose-Headers', EXPOSE_HEADERS.join(', ')); + + // Copy exposed headers from the target response + for (const header of EXPOSE_HEADERS) { + // Skip content-length as we'll use the original response's content-length + if (header === 'content-length') { + continue; + } + + if (response.headers.has(header)) { + responseHeaders.set(header, response.headers.get(header)!); + } + } + + // If the response was redirected, add the x-redirected-url header + if (response.redirected) { + responseHeaders.set('x-redirected-url', response.url); + } + + console.log('Response headers:', Object.fromEntries(responseHeaders.entries())); + + // Return the response with the target's body stream piped directly return new Response(response.body, { status: response.status, + statusText: response.statusText, headers: responseHeaders, }); } catch (error) { - console.error('Git proxy error:', error); - return json({ error: 'Proxy error' }, { status: 500 }); + console.error('Proxy error:', error); + return json( + { + error: 'Proxy error', + message: error instanceof Error ? error.message : 'Unknown error', + url: path ? `https://${path}` : 'Invalid URL', + }, + { status: 500 }, + ); } }