From 60b6f476d7515c34a1aafe65a55f6661e00ff8cc Mon Sep 17 00:00:00 2001
From: Leex
Date: Mon, 3 Mar 2025 15:57:25 +0100
Subject: [PATCH 01/19] 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 02/19] 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 03/19] 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 04/19] 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 05/19] 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 06/19] 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 07/19] 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 08/19] 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 09/19] 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 10/19] 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 11/19] 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 },
+ );
}
}
From 7ff48e1d45ff3ab9b7394218c230948b318ed2e1 Mon Sep 17 00:00:00 2001
From: Anirban Kar
Date: Sat, 8 Mar 2025 12:44:46 +0530
Subject: [PATCH 12/19] fix: attachment not getting sent on first message if
starter template is turned on (#1472)
---
app/components/chat/Chat.client.tsx | 29 ++++++++++++++++++++++++++++-
1 file changed, 28 insertions(+), 1 deletion(-)
diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx
index c08dd6d8..8ac5d286 100644
--- a/app/components/chat/Chat.client.tsx
+++ b/app/components/chat/Chat.client.tsx
@@ -323,7 +323,16 @@ export const ChatImpl = memo(
{
id: `1-${new Date().getTime()}`,
role: 'user',
- content: messageContent,
+ content: [
+ {
+ type: 'text',
+ text: `[Model: ${model}]\n\n[Provider: ${provider.name}]\n\n${messageContent}`,
+ },
+ ...imageDataList.map((imageData) => ({
+ type: 'image',
+ image: imageData,
+ })),
+ ] as any,
},
{
id: `2-${new Date().getTime()}`,
@@ -338,6 +347,15 @@ export const ChatImpl = memo(
},
]);
reload();
+ setInput('');
+ Cookies.remove(PROMPT_COOKIE_KEY);
+
+ setUploadedFiles([]);
+ setImageDataList([]);
+
+ resetEnhancer();
+
+ textareaRef.current?.blur();
setFakeLoading(false);
return;
@@ -364,6 +382,15 @@ export const ChatImpl = memo(
]);
reload();
setFakeLoading(false);
+ setInput('');
+ Cookies.remove(PROMPT_COOKIE_KEY);
+
+ setUploadedFiles([]);
+ setImageDataList([]);
+
+ resetEnhancer();
+
+ textareaRef.current?.blur();
return;
}
From 50dd74de071dba4ed90a42a25884a2e9c0128cc2 Mon Sep 17 00:00:00 2001
From: Stijnus <72551117+Stijnus@users.noreply.github.com>
Date: Sat, 8 Mar 2025 20:37:56 +0100
Subject: [PATCH 13/19] fix: settings bugfix error building my application
issue #1414 (#1436)
* Fix: error building my application #1414
* fix for vite
* Update vite.config.ts
* Update root.tsx
* fix the root.tsx and the debugtab
* lm studio fix and fix for the api key
* Update api.enhancer for prompt enhancement
* bugfixes
* Revert api.enhancer.ts back to original code
* Update api.enhancer.ts
* Update api.git-proxy.$.ts
* Update api.git-proxy.$.ts
* Update api.enhancer.ts
---
.../tabs/connections/ConnectionsTab.tsx | 25 +-
.../tabs/connections/GithubConnection.tsx | 2 +-
.../tabs/connections/NetlifyConnection.tsx | 2 +-
app/lib/hooks/useShortcuts.ts | 2 +-
app/lib/modules/llm/providers/lmstudio.ts | 2 +-
app/lib/modules/llm/providers/ollama.ts | 31 +-
app/root.tsx | 7 +-
app/routes/api.check-env-key.ts | 33 +-
app/routes/api.deploy.ts | 12 +-
app/routes/api.enhancer.ts | 18 +-
app/routes/api.git-proxy.$.ts | 8 +-
app/routes/api.health.ts | 20 +-
app/routes/api.system.app-info.ts | 47 +-
app/routes/api.system.git-info.ts | 168 ++---
app/routes/api.update.ts | 580 +-----------------
package.json | 1 +
pnpm-lock.yaml | 3 +
vite.config.ts | 43 +-
18 files changed, 233 insertions(+), 771 deletions(-)
diff --git a/app/components/@settings/tabs/connections/ConnectionsTab.tsx b/app/components/@settings/tabs/connections/ConnectionsTab.tsx
index 450d241a..72ff6434 100644
--- a/app/components/@settings/tabs/connections/ConnectionsTab.tsx
+++ b/app/components/@settings/tabs/connections/ConnectionsTab.tsx
@@ -1,6 +1,19 @@
import { motion } from 'framer-motion';
-import { GithubConnection } from './GithubConnection';
-import { NetlifyConnection } from './NetlifyConnection';
+import React, { Suspense } from 'react';
+
+// Use React.lazy for dynamic imports
+const GithubConnection = React.lazy(() => import('./GithubConnection'));
+const NetlifyConnection = React.lazy(() => import('./NetlifyConnection'));
+
+// Loading fallback component
+const LoadingFallback = () => (
+
+
+
+
Loading connection...
+
+
+);
export default function ConnectionsTab() {
return (
@@ -20,8 +33,12 @@ export default function ConnectionsTab() {
-
-
+ }>
+
+
+ }>
+
+
);
diff --git a/app/components/@settings/tabs/connections/GithubConnection.tsx b/app/components/@settings/tabs/connections/GithubConnection.tsx
index 36197f2e..789cf0b1 100644
--- a/app/components/@settings/tabs/connections/GithubConnection.tsx
+++ b/app/components/@settings/tabs/connections/GithubConnection.tsx
@@ -66,7 +66,7 @@ interface GitHubConnection {
stats?: GitHubStats;
}
-export function GithubConnection() {
+export default function GithubConnection() {
const [connection, setConnection] = useState({
user: null,
token: '',
diff --git a/app/components/@settings/tabs/connections/NetlifyConnection.tsx b/app/components/@settings/tabs/connections/NetlifyConnection.tsx
index 5881b761..d811602e 100644
--- a/app/components/@settings/tabs/connections/NetlifyConnection.tsx
+++ b/app/components/@settings/tabs/connections/NetlifyConnection.tsx
@@ -13,7 +13,7 @@ import {
} from '~/lib/stores/netlify';
import type { NetlifyUser } from '~/types/netlify';
-export function NetlifyConnection() {
+export default function NetlifyConnection() {
const connection = useStore(netlifyConnection);
const connecting = useStore(isConnecting);
const fetchingStats = useStore(isFetchingStats);
diff --git a/app/lib/hooks/useShortcuts.ts b/app/lib/hooks/useShortcuts.ts
index 39308fcd..447a97e3 100644
--- a/app/lib/hooks/useShortcuts.ts
+++ b/app/lib/hooks/useShortcuts.ts
@@ -41,7 +41,7 @@ export function useShortcuts(): void {
}
// Debug logging in development only
- if (process.env.NODE_ENV === 'development') {
+ if (import.meta.env.DEV) {
console.log('Key pressed:', {
key: event.key,
code: event.code,
diff --git a/app/lib/modules/llm/providers/lmstudio.ts b/app/lib/modules/llm/providers/lmstudio.ts
index 9dabc3eb..fe5b27cd 100644
--- a/app/lib/modules/llm/providers/lmstudio.ts
+++ b/app/lib/modules/llm/providers/lmstudio.ts
@@ -75,7 +75,7 @@ export default class LMStudioProvider extends BaseProvider {
throw new Error('No baseUrl found for LMStudio provider');
}
- const isDocker = process.env.RUNNING_IN_DOCKER === 'true' || serverEnv?.RUNNING_IN_DOCKER === 'true';
+ const isDocker = process?.env?.RUNNING_IN_DOCKER === 'true' || serverEnv?.RUNNING_IN_DOCKER === 'true';
if (typeof window === 'undefined') {
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
diff --git a/app/lib/modules/llm/providers/ollama.ts b/app/lib/modules/llm/providers/ollama.ts
index a3974ab3..e50ecae5 100644
--- a/app/lib/modules/llm/providers/ollama.ts
+++ b/app/lib/modules/llm/providers/ollama.ts
@@ -27,8 +27,6 @@ export interface OllamaApiResponse {
models: OllamaModel[];
}
-export const DEFAULT_NUM_CTX = process?.env?.DEFAULT_NUM_CTX ? parseInt(process.env.DEFAULT_NUM_CTX, 10) : 32768;
-
export default class OllamaProvider extends BaseProvider {
name = 'Ollama';
getApiKeyLink = 'https://ollama.com/download';
@@ -41,6 +39,26 @@ export default class OllamaProvider extends BaseProvider {
staticModels: ModelInfo[] = [];
+ private _convertEnvToRecord(env?: Env): Record {
+ if (!env) {
+ return {};
+ }
+
+ // Convert Env to a plain object with string values
+ return Object.entries(env).reduce(
+ (acc, [key, value]) => {
+ acc[key] = String(value);
+ return acc;
+ },
+ {} as Record,
+ );
+ }
+
+ getDefaultNumCtx(serverEnv?: Env): number {
+ const envRecord = this._convertEnvToRecord(serverEnv);
+ return envRecord.DEFAULT_NUM_CTX ? parseInt(envRecord.DEFAULT_NUM_CTX, 10) : 32768;
+ }
+
async getDynamicModels(
apiKeys?: Record,
settings?: IProviderSetting,
@@ -81,6 +99,7 @@ export default class OllamaProvider extends BaseProvider {
maxTokenAllowed: 8000,
}));
}
+
getModelInstance: (options: {
model: string;
serverEnv?: Env;
@@ -88,10 +107,12 @@ export default class OllamaProvider extends BaseProvider {
providerSettings?: Record;
}) => LanguageModelV1 = (options) => {
const { apiKeys, providerSettings, serverEnv, model } = options;
+ const envRecord = this._convertEnvToRecord(serverEnv);
+
let { baseUrl } = this.getProviderBaseUrlAndKey({
apiKeys,
providerSettings: providerSettings?.[this.name],
- serverEnv: serverEnv as any,
+ serverEnv: envRecord,
defaultBaseUrlKey: 'OLLAMA_API_BASE_URL',
defaultApiTokenKey: '',
});
@@ -101,14 +122,14 @@ export default class OllamaProvider extends BaseProvider {
throw new Error('No baseUrl found for OLLAMA provider');
}
- const isDocker = process?.env?.RUNNING_IN_DOCKER === 'true' || serverEnv?.RUNNING_IN_DOCKER === 'true';
+ const isDocker = process?.env?.RUNNING_IN_DOCKER === 'true' || envRecord.RUNNING_IN_DOCKER === 'true';
baseUrl = isDocker ? baseUrl.replace('localhost', 'host.docker.internal') : baseUrl;
baseUrl = isDocker ? baseUrl.replace('127.0.0.1', 'host.docker.internal') : baseUrl;
logger.debug('Ollama Base Url used: ', baseUrl);
const ollamaInstance = ollama(model, {
- numCtx: DEFAULT_NUM_CTX,
+ numCtx: this.getDefaultNumCtx(serverEnv),
}) as LanguageModelV1 & { config: any };
ollamaInstance.config.baseURL = `${baseUrl}/api`;
diff --git a/app/root.tsx b/app/root.tsx
index b49d7355..a7ccb285 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -8,6 +8,7 @@ import { createHead } from 'remix-island';
import { useEffect } from 'react';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
+import { ClientOnly } from 'remix-utils/client-only';
import reactToastifyStyles from 'react-toastify/dist/ReactToastify.css?url';
import globalStyles from './styles/index.scss?url';
@@ -72,11 +73,11 @@ export function Layout({ children }: { children: React.ReactNode }) {
}, [theme]);
return (
-
- {children}
+ <>
+ {() => {children}}
-
+ >
);
}
diff --git a/app/routes/api.check-env-key.ts b/app/routes/api.check-env-key.ts
index 14d21236..70f65bf0 100644
--- a/app/routes/api.check-env-key.ts
+++ b/app/routes/api.check-env-key.ts
@@ -1,16 +1,41 @@
import type { LoaderFunction } from '@remix-run/cloudflare';
-import { providerBaseUrlEnvKeys } from '~/utils/constants';
+import { LLMManager } from '~/lib/modules/llm/manager';
+import { getApiKeysFromCookie } from '~/lib/api/cookies';
export const loader: LoaderFunction = async ({ context, request }) => {
const url = new URL(request.url);
const provider = url.searchParams.get('provider');
- if (!provider || !providerBaseUrlEnvKeys[provider].apiTokenKey) {
+ if (!provider) {
return Response.json({ isSet: false });
}
- const envVarName = providerBaseUrlEnvKeys[provider].apiTokenKey;
- const isSet = !!(process.env[envVarName] || (context?.cloudflare?.env as Record)?.[envVarName]);
+ const llmManager = LLMManager.getInstance(context?.cloudflare?.env as any);
+ const providerInstance = llmManager.getProvider(provider);
+
+ if (!providerInstance || !providerInstance.config.apiTokenKey) {
+ return Response.json({ isSet: false });
+ }
+
+ const envVarName = providerInstance.config.apiTokenKey;
+
+ // Get API keys from cookie
+ const cookieHeader = request.headers.get('Cookie');
+ const apiKeys = getApiKeysFromCookie(cookieHeader);
+
+ /*
+ * Check API key in order of precedence:
+ * 1. Client-side API keys (from cookies)
+ * 2. Server environment variables (from Cloudflare env)
+ * 3. Process environment variables (from .env.local)
+ * 4. LLMManager environment variables
+ */
+ const isSet = !!(
+ apiKeys?.[provider] ||
+ (context?.cloudflare?.env as Record)?.[envVarName] ||
+ process.env[envVarName] ||
+ llmManager.env[envVarName]
+ );
return Response.json({ isSet });
};
diff --git a/app/routes/api.deploy.ts b/app/routes/api.deploy.ts
index 48543e97..0bc1c5e4 100644
--- a/app/routes/api.deploy.ts
+++ b/app/routes/api.deploy.ts
@@ -1,5 +1,4 @@
import { type ActionFunctionArgs, json } from '@remix-run/cloudflare';
-import crypto from 'crypto';
import type { NetlifySiteInfo } from '~/types/netlify';
interface DeployRequestBody {
@@ -8,6 +7,15 @@ interface DeployRequestBody {
chatId: string;
}
+async function sha1(message: string) {
+ const msgBuffer = new TextEncoder().encode(message);
+ const hashBuffer = await crypto.subtle.digest('SHA-1', msgBuffer);
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
+
+ return hashHex;
+}
+
export async function action({ request }: ActionFunctionArgs) {
try {
const { siteId, files, token, chatId } = (await request.json()) as DeployRequestBody & { token: string };
@@ -104,7 +112,7 @@ export async function action({ request }: ActionFunctionArgs) {
for (const [filePath, content] of Object.entries(files)) {
// Ensure file path starts with a forward slash
const normalizedPath = filePath.startsWith('/') ? filePath : '/' + filePath;
- const hash = crypto.createHash('sha1').update(content).digest('hex');
+ const hash = await sha1(content);
fileDigests[normalizedPath] = hash;
}
diff --git a/app/routes/api.enhancer.ts b/app/routes/api.enhancer.ts
index 4ab54f31..1e7bad5f 100644
--- a/app/routes/api.enhancer.ts
+++ b/app/routes/api.enhancer.ts
@@ -95,24 +95,28 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
},
});
+ // Handle streaming errors in a non-blocking way
(async () => {
- for await (const part of result.fullStream) {
- if (part.type === 'error') {
- const error: any = part.error;
- logger.error(error);
-
- return;
+ try {
+ for await (const part of result.fullStream) {
+ if (part.type === 'error') {
+ const error: any = part.error;
+ logger.error('Streaming error:', error);
+ break;
+ }
}
+ } catch (error) {
+ logger.error('Error processing stream:', error);
}
})();
+ // Return the text stream directly since it's already text data
return new Response(result.textStream, {
status: 200,
headers: {
'Content-Type': 'text/event-stream',
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
- 'Text-Encoding': 'chunked',
},
});
} catch (error: unknown) {
diff --git a/app/routes/api.git-proxy.$.ts b/app/routes/api.git-proxy.$.ts
index 45230520..a4e8f1e0 100644
--- a/app/routes/api.git-proxy.$.ts
+++ b/app/routes/api.git-proxy.$.ts
@@ -114,10 +114,14 @@ async function handleProxyRequest(request: Request, path: string | undefined) {
redirect: 'follow',
};
- // Add body and duplex option for non-GET/HEAD requests
+ // Add body 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
+
+ /*
+ * Note: duplex property is removed to ensure TypeScript compatibility
+ * across different environments and versions
+ */
}
// Forward the request to the target URL
diff --git a/app/routes/api.health.ts b/app/routes/api.health.ts
index 9d3bd839..e5f5a30d 100644
--- a/app/routes/api.health.ts
+++ b/app/routes/api.health.ts
@@ -1,18 +1,8 @@
-import type { LoaderFunctionArgs } from '@remix-run/node';
+import { json, type LoaderFunctionArgs } from '@remix-run/cloudflare';
export const loader = async ({ request: _request }: LoaderFunctionArgs) => {
- // Return a simple 200 OK response with some basic health information
- return new Response(
- JSON.stringify({
- status: 'healthy',
- timestamp: new Date().toISOString(),
- uptime: process.uptime(),
- }),
- {
- status: 200,
- headers: {
- 'Content-Type': 'application/json',
- },
- },
- );
+ return json({
+ status: 'healthy',
+ timestamp: new Date().toISOString(),
+ });
};
diff --git a/app/routes/api.system.app-info.ts b/app/routes/api.system.app-info.ts
index d3ed0185..01953b68 100644
--- a/app/routes/api.system.app-info.ts
+++ b/app/routes/api.system.app-info.ts
@@ -1,6 +1,5 @@
import type { ActionFunctionArgs, LoaderFunction } from '@remix-run/cloudflare';
import { json } from '@remix-run/cloudflare';
-import { execSync } from 'child_process';
// These are injected by Vite at build time
declare const __APP_VERSION: string;
@@ -11,34 +10,24 @@ declare const __PKG_DEPENDENCIES: Record;
declare const __PKG_DEV_DEPENDENCIES: Record;
declare const __PKG_PEER_DEPENDENCIES: Record;
declare const __PKG_OPTIONAL_DEPENDENCIES: Record;
+declare const __COMMIT_HASH: string;
+declare const __GIT_BRANCH: string;
+declare const __GIT_COMMIT_TIME: string;
+declare const __GIT_AUTHOR: string;
+declare const __GIT_EMAIL: string;
+declare const __GIT_REMOTE_URL: string;
+declare const __GIT_REPO_NAME: string;
const getGitInfo = () => {
- try {
- return {
- commitHash: execSync('git rev-parse --short HEAD').toString().trim(),
- branch: execSync('git rev-parse --abbrev-ref HEAD').toString().trim(),
- commitTime: execSync('git log -1 --format=%cd').toString().trim(),
- author: execSync('git log -1 --format=%an').toString().trim(),
- email: execSync('git log -1 --format=%ae').toString().trim(),
- remoteUrl: execSync('git config --get remote.origin.url').toString().trim(),
- repoName: execSync('git config --get remote.origin.url')
- .toString()
- .trim()
- .replace(/^.*github.com[:/]/, '')
- .replace(/\.git$/, ''),
- };
- } catch (error) {
- console.error('Failed to get git info:', error);
- return {
- commitHash: 'unknown',
- branch: 'unknown',
- commitTime: 'unknown',
- author: 'unknown',
- email: 'unknown',
- remoteUrl: 'unknown',
- repoName: 'unknown',
- };
- }
+ return {
+ commitHash: __COMMIT_HASH || 'unknown',
+ branch: __GIT_BRANCH || 'unknown',
+ commitTime: __GIT_COMMIT_TIME || 'unknown',
+ author: __GIT_AUTHOR || 'unknown',
+ email: __GIT_EMAIL || 'unknown',
+ remoteUrl: __GIT_REMOTE_URL || 'unknown',
+ repoName: __GIT_REPO_NAME || 'unknown',
+ };
};
const formatDependencies = (
@@ -60,11 +49,11 @@ const getAppResponse = () => {
version: __APP_VERSION || '0.1.0',
description: __PKG_DESCRIPTION || 'A DIY LLM interface',
license: __PKG_LICENSE || 'MIT',
- environment: process.env.NODE_ENV || 'development',
+ environment: 'cloudflare',
gitInfo,
timestamp: new Date().toISOString(),
runtimeInfo: {
- nodeVersion: process.version || 'unknown',
+ nodeVersion: 'cloudflare',
},
dependencies: {
production: formatDependencies(__PKG_DEPENDENCIES, 'production'),
diff --git a/app/routes/api.system.git-info.ts b/app/routes/api.system.git-info.ts
index d6bf9975..63c879ce 100644
--- a/app/routes/api.system.git-info.ts
+++ b/app/routes/api.system.git-info.ts
@@ -1,138 +1,48 @@
-import type { LoaderFunction } from '@remix-run/cloudflare';
-import { json } from '@remix-run/cloudflare';
-import { execSync } from 'child_process';
+import { json, type LoaderFunction } from '@remix-run/cloudflare';
-interface GitHubRepoInfo {
- name: string;
- full_name: string;
- default_branch: string;
- stargazers_count: number;
- forks_count: number;
- open_issues_count: number;
- parent?: {
- full_name: string;
- default_branch: string;
- stargazers_count: number;
- forks_count: number;
+interface GitInfo {
+ local: {
+ commitHash: string;
+ branch: string;
+ commitTime: string;
+ author: string;
+ email: string;
+ remoteUrl: string;
+ repoName: string;
};
+ github?: {
+ currentRepo?: {
+ fullName: string;
+ defaultBranch: string;
+ stars: number;
+ forks: number;
+ openIssues?: number;
+ };
+ };
+ isForked?: boolean;
}
-const getLocalGitInfo = () => {
- try {
- return {
- commitHash: execSync('git rev-parse HEAD').toString().trim(),
- branch: execSync('git rev-parse --abbrev-ref HEAD').toString().trim(),
- commitTime: execSync('git log -1 --format=%cd').toString().trim(),
- author: execSync('git log -1 --format=%an').toString().trim(),
- email: execSync('git log -1 --format=%ae').toString().trim(),
- remoteUrl: execSync('git config --get remote.origin.url').toString().trim(),
- repoName: execSync('git config --get remote.origin.url')
- .toString()
- .trim()
- .replace(/^.*github.com[:/]/, '')
- .replace(/\.git$/, ''),
- };
- } catch (error) {
- console.error('Failed to get local git info:', error);
- return null;
- }
-};
+// These values will be replaced at build time
+declare const __COMMIT_HASH: string;
+declare const __GIT_BRANCH: string;
+declare const __GIT_COMMIT_TIME: string;
+declare const __GIT_AUTHOR: string;
+declare const __GIT_EMAIL: string;
+declare const __GIT_REMOTE_URL: string;
+declare const __GIT_REPO_NAME: string;
-const getGitHubInfo = async (repoFullName: string) => {
- try {
- // Add GitHub token if available
- const headers: Record = {
- Accept: 'application/vnd.github.v3+json',
- };
-
- const githubToken = process.env.GITHUB_TOKEN;
-
- if (githubToken) {
- headers.Authorization = `token ${githubToken}`;
- }
-
- console.log('Fetching GitHub info for:', repoFullName); // Debug log
-
- const response = await fetch(`https://api.github.com/repos/${repoFullName}`, {
- headers,
- });
-
- if (!response.ok) {
- console.error('GitHub API error:', {
- status: response.status,
- statusText: response.statusText,
- repoFullName,
- });
-
- // If we get a 404, try the main repo as fallback
- if (response.status === 404 && repoFullName !== 'stackblitz-labs/bolt.diy') {
- return getGitHubInfo('stackblitz-labs/bolt.diy');
- }
-
- throw new Error(`GitHub API error: ${response.statusText}`);
- }
-
- const data = await response.json();
- console.log('GitHub API response:', data); // Debug log
-
- return data as GitHubRepoInfo;
- } catch (error) {
- console.error('Failed to get GitHub info:', error);
- return null;
- }
-};
-
-export const loader: LoaderFunction = async ({ request: _request }) => {
- const localInfo = getLocalGitInfo();
- console.log('Local git info:', localInfo); // Debug log
-
- // If we have local info, try to get GitHub info for both our fork and upstream
- let githubInfo = null;
-
- if (localInfo?.repoName) {
- githubInfo = await getGitHubInfo(localInfo.repoName);
- }
-
- // If no local info or GitHub info, try the main repo
- if (!githubInfo) {
- githubInfo = await getGitHubInfo('stackblitz-labs/bolt.diy');
- }
-
- const response = {
- local: localInfo || {
- commitHash: 'unknown',
- branch: 'unknown',
- commitTime: 'unknown',
- author: 'unknown',
- email: 'unknown',
- remoteUrl: 'unknown',
- repoName: 'unknown',
+export const loader: LoaderFunction = async () => {
+ const gitInfo: GitInfo = {
+ local: {
+ commitHash: typeof __COMMIT_HASH !== 'undefined' ? __COMMIT_HASH : 'development',
+ branch: typeof __GIT_BRANCH !== 'undefined' ? __GIT_BRANCH : 'main',
+ commitTime: typeof __GIT_COMMIT_TIME !== 'undefined' ? __GIT_COMMIT_TIME : new Date().toISOString(),
+ author: typeof __GIT_AUTHOR !== 'undefined' ? __GIT_AUTHOR : 'development',
+ email: typeof __GIT_EMAIL !== 'undefined' ? __GIT_EMAIL : 'development@local',
+ remoteUrl: typeof __GIT_REMOTE_URL !== 'undefined' ? __GIT_REMOTE_URL : 'local',
+ repoName: typeof __GIT_REPO_NAME !== 'undefined' ? __GIT_REPO_NAME : 'bolt.diy',
},
- github: githubInfo
- ? {
- currentRepo: {
- fullName: githubInfo.full_name,
- defaultBranch: githubInfo.default_branch,
- stars: githubInfo.stargazers_count,
- forks: githubInfo.forks_count,
- openIssues: githubInfo.open_issues_count,
- },
- upstream: githubInfo.parent
- ? {
- fullName: githubInfo.parent.full_name,
- defaultBranch: githubInfo.parent.default_branch,
- stars: githubInfo.parent.stargazers_count,
- forks: githubInfo.parent.forks_count,
- }
- : null,
- }
- : null,
- isForked: Boolean(githubInfo?.parent),
- timestamp: new Date().toISOString(),
};
- console.log('Final response:', response);
-
- // Debug log
- return json(response);
+ return json(gitInfo);
};
diff --git a/app/routes/api.update.ts b/app/routes/api.update.ts
index 9f79d4ae..97d28ce0 100644
--- a/app/routes/api.update.ts
+++ b/app/routes/api.update.ts
@@ -1,573 +1,21 @@
-import { json } from '@remix-run/node';
-import type { ActionFunction } from '@remix-run/node';
-import { exec } from 'child_process';
-import { promisify } from 'util';
-
-const execAsync = promisify(exec);
-
-interface UpdateRequestBody {
- branch: string;
- autoUpdate?: boolean;
-}
-
-interface UpdateProgress {
- stage: 'fetch' | 'pull' | 'install' | 'build' | 'complete';
- message: string;
- progress?: number;
- error?: string;
- details?: {
- changedFiles?: string[];
- additions?: number;
- deletions?: number;
- commitMessages?: string[];
- totalSize?: string;
- currentCommit?: string;
- remoteCommit?: string;
- updateReady?: boolean;
- changelog?: string;
- compareUrl?: string;
- };
-}
+import { json, type ActionFunction } from '@remix-run/cloudflare';
export const action: ActionFunction = async ({ request }) => {
if (request.method !== 'POST') {
return json({ error: 'Method not allowed' }, { status: 405 });
}
- try {
- const body = await request.json();
-
- if (!body || typeof body !== 'object' || !('branch' in body) || typeof body.branch !== 'string') {
- return json({ error: 'Invalid request body: branch is required and must be a string' }, { status: 400 });
- }
-
- const { branch, autoUpdate = false } = body as UpdateRequestBody;
-
- // Create a ReadableStream to send progress updates
- const stream = new ReadableStream({
- async start(controller) {
- const encoder = new TextEncoder();
- const sendProgress = (update: UpdateProgress) => {
- controller.enqueue(encoder.encode(JSON.stringify(update) + '\n'));
- };
-
- try {
- // Initial check stage
- sendProgress({
- stage: 'fetch',
- message: 'Checking repository status...',
- progress: 0,
- });
-
- // Check if remote exists
- let defaultBranch = branch || 'main'; // Make branch mutable
-
- try {
- await execAsync('git remote get-url upstream');
- sendProgress({
- stage: 'fetch',
- message: 'Repository remote verified',
- progress: 10,
- });
- } catch {
- throw new Error(
- 'No upstream repository found. Please set up the upstream repository first by running:\ngit remote add upstream https://github.com/stackblitz-labs/bolt.diy.git',
- );
- }
-
- // Get default branch if not specified
- if (!branch) {
- sendProgress({
- stage: 'fetch',
- message: 'Detecting default branch...',
- progress: 20,
- });
-
- try {
- const { stdout } = await execAsync('git remote show upstream | grep "HEAD branch" | cut -d" " -f5');
- defaultBranch = stdout.trim() || 'main';
- sendProgress({
- stage: 'fetch',
- message: `Using branch: ${defaultBranch}`,
- progress: 30,
- });
- } catch {
- defaultBranch = 'main'; // Fallback to main if we can't detect
- sendProgress({
- stage: 'fetch',
- message: 'Using default branch: main',
- progress: 30,
- });
- }
- }
-
- // Fetch stage
- sendProgress({
- stage: 'fetch',
- message: 'Fetching latest changes...',
- progress: 40,
- });
-
- // Fetch all remotes
- await execAsync('git fetch --all');
- sendProgress({
- stage: 'fetch',
- message: 'Remote changes fetched',
- progress: 50,
- });
-
- // Check if remote branch exists
- try {
- await execAsync(`git rev-parse --verify upstream/${defaultBranch}`);
- sendProgress({
- stage: 'fetch',
- message: 'Remote branch verified',
- progress: 60,
- });
- } catch {
- throw new Error(
- `Remote branch 'upstream/${defaultBranch}' not found. Please ensure the upstream repository is properly configured.`,
- );
- }
-
- // Get current commit hash and remote commit hash
- sendProgress({
- stage: 'fetch',
- message: 'Comparing versions...',
- progress: 70,
- });
-
- const { stdout: currentCommit } = await execAsync('git rev-parse HEAD');
- const { stdout: remoteCommit } = await execAsync(`git rev-parse upstream/${defaultBranch}`);
-
- // If we're on the same commit, no update is available
- if (currentCommit.trim() === remoteCommit.trim()) {
- sendProgress({
- stage: 'complete',
- message: 'No updates available. You are on the latest version.',
- progress: 100,
- details: {
- currentCommit: currentCommit.trim().substring(0, 7),
- remoteCommit: remoteCommit.trim().substring(0, 7),
- },
- });
- return;
- }
-
- sendProgress({
- stage: 'fetch',
- message: 'Analyzing changes...',
- progress: 80,
- });
-
- // Initialize variables
- let changedFiles: string[] = [];
- let commitMessages: string[] = [];
- let stats: RegExpMatchArray | null = null;
- let totalSizeInBytes = 0;
-
- // Format size for display
- const formatSize = (bytes: number) => {
- if (bytes === 0) {
- return '0 B';
- }
-
- const k = 1024;
- const sizes = ['B', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
-
- return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
- };
-
- // Get list of changed files and their sizes
- try {
- const { stdout: diffOutput } = await execAsync(
- `git diff --name-status ${currentCommit.trim()}..${remoteCommit.trim()}`,
- );
- const files = diffOutput.split('\n').filter(Boolean);
-
- if (files.length === 0) {
- sendProgress({
- stage: 'complete',
- message: `No file changes detected between your version and upstream/${defaultBranch}. You might be on a different branch.`,
- progress: 100,
- details: {
- currentCommit: currentCommit.trim().substring(0, 7),
- remoteCommit: remoteCommit.trim().substring(0, 7),
- },
- });
- return;
- }
-
- sendProgress({
- stage: 'fetch',
- message: `Found ${files.length} changed files, calculating sizes...`,
- progress: 90,
- });
-
- // Get size information for each changed file
- for (const line of files) {
- const [status, file] = line.split('\t');
-
- if (status !== 'D') {
- // Skip deleted files
- try {
- const { stdout: sizeOutput } = await execAsync(`git cat-file -s ${remoteCommit.trim()}:${file}`);
- const size = parseInt(sizeOutput) || 0;
- totalSizeInBytes += size;
- } catch {
- console.debug(`Could not get size for file: ${file}`);
- }
- }
- }
-
- changedFiles = files.map((line) => {
- const [status, file] = line.split('\t');
- return `${status === 'M' ? 'Modified' : status === 'A' ? 'Added' : 'Deleted'}: ${file}`;
- });
- } catch (err) {
- console.debug('Failed to get changed files:', err);
- throw new Error(`Failed to compare changes with upstream/${defaultBranch}. Are you on the correct branch?`);
- }
-
- // Get commit messages between current and remote
- try {
- const { stdout: logOutput } = await execAsync(
- `git log --pretty=format:"%h|%s|%aI" ${currentCommit.trim()}..${remoteCommit.trim()}`,
- );
-
- // Parse and group commits by type
- const commits = logOutput
- .split('\n')
- .filter(Boolean)
- .map((line) => {
- const [hash, subject, timestamp] = line.split('|');
- let type = 'other';
- let message = subject;
-
- if (subject.startsWith('feat:') || subject.startsWith('feature:')) {
- type = 'feature';
- message = subject.replace(/^feat(?:ure)?:/, '').trim();
- } else if (subject.startsWith('fix:')) {
- type = 'fix';
- message = subject.replace(/^fix:/, '').trim();
- } else if (subject.startsWith('docs:')) {
- type = 'docs';
- message = subject.replace(/^docs:/, '').trim();
- } else if (subject.startsWith('style:')) {
- type = 'style';
- message = subject.replace(/^style:/, '').trim();
- } else if (subject.startsWith('refactor:')) {
- type = 'refactor';
- message = subject.replace(/^refactor:/, '').trim();
- } else if (subject.startsWith('perf:')) {
- type = 'perf';
- message = subject.replace(/^perf:/, '').trim();
- } else if (subject.startsWith('test:')) {
- type = 'test';
- message = subject.replace(/^test:/, '').trim();
- } else if (subject.startsWith('build:')) {
- type = 'build';
- message = subject.replace(/^build:/, '').trim();
- } else if (subject.startsWith('ci:')) {
- type = 'ci';
- message = subject.replace(/^ci:/, '').trim();
- }
-
- return {
- hash,
- type,
- message,
- timestamp: new Date(timestamp),
- };
- });
-
- // Group commits by type
- const groupedCommits = commits.reduce(
- (acc, commit) => {
- if (!acc[commit.type]) {
- acc[commit.type] = [];
- }
-
- acc[commit.type].push(commit);
-
- return acc;
- },
- {} as Record,
- );
-
- // Format commit messages with emojis and timestamps
- const formattedMessages = Object.entries(groupedCommits).map(([type, commits]) => {
- const emoji = {
- feature: '✨',
- fix: '🐛',
- docs: '📚',
- style: '💎',
- refactor: '♻️',
- perf: '⚡',
- test: '🧪',
- build: '🛠️',
- ci: '⚙️',
- other: '🔍',
- }[type];
-
- const title = {
- feature: 'Features',
- fix: 'Bug Fixes',
- docs: 'Documentation',
- style: 'Styles',
- refactor: 'Code Refactoring',
- perf: 'Performance',
- test: 'Tests',
- build: 'Build',
- ci: 'CI',
- other: 'Other Changes',
- }[type];
-
- return `### ${emoji} ${title}\n\n${commits
- .map((c) => `* ${c.message} (${c.hash.substring(0, 7)}) - ${c.timestamp.toLocaleString()}`)
- .join('\n')}`;
- });
-
- commitMessages = formattedMessages;
- } catch {
- // Handle silently - empty commitMessages array will be used
- }
-
- // Get diff stats using the specific commits
- try {
- const { stdout: diffStats } = await execAsync(
- `git diff --shortstat ${currentCommit.trim()}..${remoteCommit.trim()}`,
- );
- stats = diffStats.match(
- /(\d+) files? changed(?:, (\d+) insertions?\\(\\+\\))?(?:, (\d+) deletions?\\(-\\))?/,
- );
- } catch {
- // Handle silently - null stats will be used
- }
-
- // If we somehow still have no changes detected
- if (!stats && changedFiles.length === 0) {
- sendProgress({
- stage: 'complete',
- message: `No changes detected between your version and upstream/${defaultBranch}. This might be unexpected - please check your git status.`,
- progress: 100,
- });
- return;
- }
-
- // Fetch changelog
- sendProgress({
- stage: 'fetch',
- message: 'Fetching changelog...',
- progress: 95,
- });
-
- const changelog = await fetchChangelog(currentCommit.trim(), remoteCommit.trim());
-
- // We have changes, send the details
- sendProgress({
- stage: 'fetch',
- message: `Changes detected on upstream/${defaultBranch}`,
- progress: 100,
- details: {
- changedFiles,
- additions: stats?.[2] ? parseInt(stats[2]) : 0,
- deletions: stats?.[3] ? parseInt(stats[3]) : 0,
- commitMessages,
- totalSize: formatSize(totalSizeInBytes),
- currentCommit: currentCommit.trim().substring(0, 7),
- remoteCommit: remoteCommit.trim().substring(0, 7),
- updateReady: true,
- changelog,
- compareUrl: `https://github.com/stackblitz-labs/bolt.diy/compare/${currentCommit.trim().substring(0, 7)}...${remoteCommit.trim().substring(0, 7)}`,
- },
- });
-
- // Only proceed with update if autoUpdate is true
- if (!autoUpdate) {
- sendProgress({
- stage: 'complete',
- message: 'Update is ready to be applied. Click "Update Now" to proceed.',
- progress: 100,
- details: {
- changedFiles,
- additions: stats?.[2] ? parseInt(stats[2]) : 0,
- deletions: stats?.[3] ? parseInt(stats[3]) : 0,
- commitMessages,
- totalSize: formatSize(totalSizeInBytes),
- currentCommit: currentCommit.trim().substring(0, 7),
- remoteCommit: remoteCommit.trim().substring(0, 7),
- updateReady: true,
- changelog,
- compareUrl: `https://github.com/stackblitz-labs/bolt.diy/compare/${currentCommit.trim().substring(0, 7)}...${remoteCommit.trim().substring(0, 7)}`,
- },
- });
- return;
- }
-
- // Pull stage
- sendProgress({
- stage: 'pull',
- message: `Pulling changes from upstream/${defaultBranch}...`,
- progress: 0,
- });
-
- await execAsync(`git pull upstream ${defaultBranch}`);
-
- sendProgress({
- stage: 'pull',
- message: 'Changes pulled successfully',
- progress: 100,
- });
-
- // Install stage
- sendProgress({
- stage: 'install',
- message: 'Installing dependencies...',
- progress: 0,
- });
-
- await execAsync('pnpm install');
-
- sendProgress({
- stage: 'install',
- message: 'Dependencies installed successfully',
- progress: 100,
- });
-
- // Build stage
- sendProgress({
- stage: 'build',
- message: 'Building application...',
- progress: 0,
- });
-
- await execAsync('pnpm build');
-
- sendProgress({
- stage: 'build',
- message: 'Build completed successfully',
- progress: 100,
- });
-
- // Complete
- sendProgress({
- stage: 'complete',
- message: 'Update completed successfully! Click Restart to apply changes.',
- progress: 100,
- });
- } catch (err) {
- sendProgress({
- stage: 'complete',
- message: 'Update failed',
- error: err instanceof Error ? err.message : 'Unknown error occurred',
- });
- } finally {
- controller.close();
- }
- },
- });
-
- return new Response(stream, {
- headers: {
- 'Content-Type': 'text/event-stream',
- 'Cache-Control': 'no-cache',
- Connection: 'keep-alive',
- },
- });
- } catch (err) {
- console.error('Update preparation failed:', err);
- return json(
- {
- success: false,
- error: err instanceof Error ? err.message : 'Unknown error occurred while preparing update',
- },
- { status: 500 },
- );
- }
+ return json(
+ {
+ error: 'Updates must be performed manually in a server environment',
+ instructions: [
+ '1. Navigate to the project directory',
+ '2. Run: git fetch upstream',
+ '3. Run: git pull upstream main',
+ '4. Run: pnpm install',
+ '5. Run: pnpm run build',
+ ],
+ },
+ { status: 400 },
+ );
};
-
-// Add this function to fetch the changelog
-async function fetchChangelog(currentCommit: string, remoteCommit: string): Promise {
- try {
- // First try to get the changelog.md content
- const { stdout: changelogContent } = await execAsync('git show upstream/main:changelog.md');
-
- // If we have a changelog, return it
- if (changelogContent) {
- return changelogContent;
- }
-
- // If no changelog.md, generate one in a similar format
- let changelog = '# Changes in this Update\n\n';
-
- // Get commit messages grouped by type
- const { stdout: commitLog } = await execAsync(
- `git log --pretty=format:"%h|%s|%b" ${currentCommit.trim()}..${remoteCommit.trim()}`,
- );
-
- const commits = commitLog.split('\n').filter(Boolean);
- const categorizedCommits: Record = {
- '✨ Features': [],
- '🐛 Bug Fixes': [],
- '📚 Documentation': [],
- '💎 Styles': [],
- '♻️ Code Refactoring': [],
- '⚡ Performance': [],
- '🧪 Tests': [],
- '🛠️ Build': [],
- '⚙️ CI': [],
- '🔍 Other Changes': [],
- };
-
- // Categorize commits
- for (const commit of commits) {
- const [hash, subject] = commit.split('|');
- let category = '🔍 Other Changes';
-
- if (subject.startsWith('feat:') || subject.startsWith('feature:')) {
- category = '✨ Features';
- } else if (subject.startsWith('fix:')) {
- category = '🐛 Bug Fixes';
- } else if (subject.startsWith('docs:')) {
- category = '📚 Documentation';
- } else if (subject.startsWith('style:')) {
- category = '💎 Styles';
- } else if (subject.startsWith('refactor:')) {
- category = '♻️ Code Refactoring';
- } else if (subject.startsWith('perf:')) {
- category = '⚡ Performance';
- } else if (subject.startsWith('test:')) {
- category = '🧪 Tests';
- } else if (subject.startsWith('build:')) {
- category = '🛠️ Build';
- } else if (subject.startsWith('ci:')) {
- category = '⚙️ CI';
- }
-
- const message = subject.includes(':') ? subject.split(':')[1].trim() : subject.trim();
- categorizedCommits[category].push(`* ${message} (${hash.substring(0, 7)})`);
- }
-
- // Build changelog content
- for (const [category, commits] of Object.entries(categorizedCommits)) {
- if (commits.length > 0) {
- changelog += `\n## ${category}\n\n${commits.join('\n')}\n`;
- }
- }
-
- // Add stats
- const { stdout: stats } = await execAsync(`git diff --shortstat ${currentCommit.trim()}..${remoteCommit.trim()}`);
-
- if (stats) {
- changelog += '\n## 📊 Stats\n\n';
- changelog += `${stats.trim()}\n`;
- }
-
- return changelog;
- } catch (error) {
- console.error('Error fetching changelog:', error);
- return 'Unable to fetch changelog';
- }
-}
diff --git a/package.json b/package.json
index e287f0c5..144831fb 100644
--- a/package.json
+++ b/package.json
@@ -123,6 +123,7 @@
"remark-gfm": "^4.0.0",
"remix-island": "^0.2.0",
"remix-utils": "^7.7.0",
+ "rollup-plugin-node-polyfills": "^0.2.1",
"shiki": "^1.24.0",
"tailwind-merge": "^2.2.1",
"unist-util-visit": "^5.0.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index fc491791..bb37289b 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -287,6 +287,9 @@ importers:
remix-utils:
specifier: ^7.7.0
version: 7.7.0(@remix-run/cloudflare@2.15.3(@cloudflare/workers-types@4.20250204.0)(typescript@5.7.3))(@remix-run/node@2.15.3(typescript@5.7.3))(@remix-run/react@2.15.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.3))(@remix-run/router@1.22.0)(react@18.3.1)(zod@3.24.1)
+ rollup-plugin-node-polyfills:
+ specifier: ^0.2.1
+ version: 0.2.1
shiki:
specifier: ^1.24.0
version: 1.29.2
diff --git a/vite.config.ts b/vite.config.ts
index 01fb3b2e..a9351c12 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -89,14 +89,55 @@ export default defineConfig((config) => {
__PKG_DEV_DEPENDENCIES: JSON.stringify(pkg.devDependencies),
__PKG_PEER_DEPENDENCIES: JSON.stringify(pkg.peerDependencies),
__PKG_OPTIONAL_DEPENDENCIES: JSON.stringify(pkg.optionalDependencies),
+ // Define global values
+ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
},
build: {
target: 'esnext',
+ rollupOptions: {
+ output: {
+ format: 'esm',
+ },
+ },
+ commonjsOptions: {
+ transformMixedEsModules: true,
+ },
+ },
+ optimizeDeps: {
+ esbuildOptions: {
+ define: {
+ global: 'globalThis',
+ },
+ },
+ },
+ resolve: {
+ alias: {
+ buffer: 'vite-plugin-node-polyfills/polyfills/buffer',
+ },
},
plugins: [
nodePolyfills({
- include: ['path', 'buffer', 'process'],
+ include: ['buffer', 'process', 'util', 'stream'],
+ globals: {
+ Buffer: true,
+ process: true,
+ global: true,
+ },
+ protocolImports: true,
+ // Exclude Node.js modules that shouldn't be polyfilled in Cloudflare
+ exclude: ['child_process', 'fs', 'path'],
}),
+ {
+ name: 'buffer-polyfill',
+ transform(code, id) {
+ if (id.includes('env.mjs')) {
+ return {
+ code: `import { Buffer } from 'buffer';\n${code}`,
+ map: null,
+ };
+ }
+ },
+ },
config.mode !== 'test' && remixCloudflareDevProxy(),
remixVitePlugin({
future: {
From fc779b5b1f49181d9a5f63130dd7815002692cef Mon Sep 17 00:00:00 2001
From: Leex
Date: Sat, 8 Mar 2025 23:08:40 +0100
Subject: [PATCH 14/19] update: docs README.md
- changed git clone for pulling stable branch, not main anymore
- Removed some not needed stuff
---
README.md | 31 ++++++++++++++++++-------------
1 file changed, 18 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md
index 3071fcee..f92a21ff 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
-# bolt.diy (Previously oTToDev)
+# bolt.diy
[](https://bolt.diy)
-Welcome to bolt.diy, the official open source version of Bolt.new (previously known as oTToDev and bolt.new ANY LLM), which allows you to choose the LLM that you use for each prompt! Currently, you can use OpenAI, Anthropic, Ollama, OpenRouter, Gemini, LMStudio, Mistral, xAI, HuggingFace, DeepSeek, or Groq models - and it is easily extended to use any other model supported by the Vercel AI SDK! See the instructions below for running this locally and extending it to include more models.
+Welcome to bolt.diy, the official open source version of Bolt.new, which allows you to choose the LLM that you use for each prompt! Currently, you can use OpenAI, Anthropic, Ollama, OpenRouter, Gemini, LMStudio, Mistral, xAI, HuggingFace, DeepSeek, or Groq models - and it is easily extended to use any other model supported by the Vercel AI SDK! See the instructions below for running this locally and extending it to include more models.
-----
Check the [bolt.diy Docs](https://stackblitz-labs.github.io/bolt.diy/) for more offical installation instructions and more informations.
@@ -83,7 +83,8 @@ project, please check the [project management guide](./PROJECT.md) to get starte
- ⬜ **HIGH PRIORITY** - Prevent bolt from rewriting files as often (file locking and diffs)
- ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
- ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
-- ⬜ Deploy directly to Vercel/Netlify/other similar platforms
+- ✅ Deploy directly to Netlify (@xKevIsDev)
+- ⬜ Supabase Integration
- ⬜ Have LLM plan the project in a MD file for better results/transparency
- ⬜ VSCode Integration with git-like confirmations
- ⬜ Upload documents for knowledge - UI design templates, a code base to reference coding style, etc.
@@ -101,8 +102,9 @@ project, please check the [project management guide](./PROJECT.md) to get starte
- **Attach images to prompts** for better contextual understanding.
- **Integrated terminal** to view output of LLM-run commands.
- **Revert code to earlier versions** for easier debugging and quicker changes.
-- **Download projects as ZIP** for easy portability.
+- **Download projects as ZIP** for easy portabilitr Sync to a folder on the host.
- **Integration-ready Docker support** for a hassle-free setup.
+- **Deploy** directly to **Netlify**
## Setup
@@ -241,8 +243,7 @@ This method is recommended for developers who want to:
1. **Clone the Repository**:
```bash
- # Using HTTPS
- git clone https://github.com/stackblitz-labs/bolt.diy.git
+ git clone -b stable https://github.com/stackblitz-labs/bolt.diy.git
```
2. **Navigate to Project Directory**:
@@ -251,21 +252,25 @@ This method is recommended for developers who want to:
cd bolt.diy
```
-3. **Switch to the Main Branch**:
- ```bash
- git checkout main
- ```
-4. **Install Dependencies**:
+3. **Install Dependencies**:
```bash
pnpm install
```
-5. **Start the Development Server**:
+4. **Start the Development Server**:
```bash
pnpm run dev
```
+5. **(OPTIONAL)** Switch to the Main Branch if you want to use pre-release/testbranch):
+ ```bash
+ git checkout main
+ ```
+ Hint: Be aware that this can have beta-features and more likely got bugs than the stable release
+
+
+
#### Staying Updated
To get the latest changes from the repository:
@@ -279,7 +284,7 @@ To get the latest changes from the repository:
2. **Pull Latest Updates**:
```bash
- git pull origin main
+ git pull
```
3. **Update Dependencies**:
From a696d5f254acc7e9ac0ad4c5e17021c2780e1074 Mon Sep 17 00:00:00 2001
From: Leex
Date: Sun, 9 Mar 2025 23:44:23 +0100
Subject: [PATCH 15/19] Update README.md
---
README.md | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index f92a21ff..f38e23c0 100644
--- a/README.md
+++ b/README.md
@@ -263,13 +263,19 @@ This method is recommended for developers who want to:
pnpm run dev
```
-5. **(OPTIONAL)** Switch to the Main Branch if you want to use pre-release/testbranch):
+5. **(OPTIONAL)** Switch to the Main Branch if you want to use pre-release/testbranch:
```bash
git checkout main
+ pnpm install
+ pnpm run dev
```
Hint: Be aware that this can have beta-features and more likely got bugs than the stable release
-
+>**Open the WebUI to test (Default: http://localhost:5173)**
+> - Beginngers:
+> - Try to use a sophisticated Provider/Model like Anthropic with Claude Sonnet 3.x Models to get best results
+> - Explanation: The System Prompt currently implemented in bolt.diy cant cover the best performance for all providers and models out there. So it works better with some models, then other, even if the models itself are perfect for >programming
+> - Future: Planned is a Plugin/Extentions-Library so there can be different System Prompts for different Models, which will help to get better results
#### Staying Updated
From 27fbfb76c4b95d55d837e78d399ea20de11e86dc Mon Sep 17 00:00:00 2001
From: Leex
Date: Mon, 10 Mar 2025 00:05:36 +0100
Subject: [PATCH 16/19] Update README.md
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index f38e23c0..c14974b2 100644
--- a/README.md
+++ b/README.md
@@ -102,7 +102,7 @@ project, please check the [project management guide](./PROJECT.md) to get starte
- **Attach images to prompts** for better contextual understanding.
- **Integrated terminal** to view output of LLM-run commands.
- **Revert code to earlier versions** for easier debugging and quicker changes.
-- **Download projects as ZIP** for easy portabilitr Sync to a folder on the host.
+- **Download projects as ZIP** for easy portability Sync to a folder on the host.
- **Integration-ready Docker support** for a hassle-free setup.
- **Deploy** directly to **Netlify**
From 7107163f3103eddcc209f6ac552f9fd85d54409c Mon Sep 17 00:00:00 2001
From: Leex
Date: Tue, 18 Mar 2025 10:54:39 +0100
Subject: [PATCH 17/19] Update README.md
---
README.md | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/README.md b/README.md
index c14974b2..15a45169 100644
--- a/README.md
+++ b/README.md
@@ -360,3 +360,9 @@ Explore upcoming features and priorities on our [Roadmap](https://roadmap.sh/r/o
## FAQ
For answers to common questions, issues, and to see a list of recommended models, visit our [FAQ Page](FAQ.md).
+
+
+# Licensing
+**Who needs a commercial WebContainer API license?**
+
+Licensing is required for production usage of the API in a commercial, for-profit setting. (Prototypes or POCs do not require a commercial license.) If you're using the API to meet the needs of your customers, prospective customers, and/or employees, you need a license to ensure compliance with our Terms of Service. Usage of the API in violation of these terms may result in your access being revoked.
From 64afda10781d96749cbae44b9b7e3e62d8e02b53 Mon Sep 17 00:00:00 2001
From: Leex
Date: Tue, 18 Mar 2025 11:40:49 +0100
Subject: [PATCH 18/19] Update README.md
Co-authored-by: patak <583075+patak-dev@users.noreply.github.com>
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 15a45169..74f807b5 100644
--- a/README.md
+++ b/README.md
@@ -365,4 +365,4 @@ For answers to common questions, issues, and to see a list of recommended models
# Licensing
**Who needs a commercial WebContainer API license?**
-Licensing is required for production usage of the API in a commercial, for-profit setting. (Prototypes or POCs do not require a commercial license.) If you're using the API to meet the needs of your customers, prospective customers, and/or employees, you need a license to ensure compliance with our Terms of Service. Usage of the API in violation of these terms may result in your access being revoked.
+bolt.diy source code is distributed as MIT, but it uses WebContainers API that [requires licensing](https://webcontainers.io/enterprise) for production usage in a commercial, for-profit setting. (Prototypes or POCs do not require a commercial license.) If you're using the API to meet the needs of your customers, prospective customers, and/or employees, you need a license to ensure compliance with our Terms of Service. Usage of the API in violation of these terms may result in your access being revoked.
From 1ce6ad6b595fb2bcec9b68fa340c038337f6e5f7 Mon Sep 17 00:00:00 2001
From: Derek Wang
Date: Wed, 19 Mar 2025 11:52:06 -0700
Subject: [PATCH 19/19] feat: electron desktop app without express server
(#1136)
* feat: add electron app
* refactor: using different approach
* chore: update commit hash to 02621e3545511ca8bc0279b70f92083218548655
* fix: working dev but prod showing not found and lint fix
* fix: add icon
* fix: resolve server file load issue
* fix: eslint and prettier wip
* fix: only load server build once
* fix: forward request for other ports
* fix: use cloudflare {} to avoid crash
* fix: no need for appLogger
* fix: forward cookie
* fix: update script and update preload loading path
* chore: minor update for appId
* fix: store and load all cookies
* refactor: split main/index.ts
* refactor: group electron main files into two folders
* fix: update electron build configs
* fix: update auto update feat
* fix: vite-plugin-node-polyfills need to be in dependencies for dmg version to work
* ci: trigger build for electron branch
* ci: mark draft if it's from branch commit
* ci: add icons for windows and linux
* fix: update icons for windows
* fix: add author in package.json
* ci: use softprops/action-gh-release@v2
* fix: use path to join
* refactor: refactor path logic for working in both mac and windows
* fix: still need vite-plugin-node-polyfills dependencies
* fix: update vite-electron.config.ts
* ci: sign mac app
* refactor: assets folder
* ci: notarization
* ci: add NODE_OPTIONS
* ci: window only nsis dist
---------
Co-authored-by: github-actions[bot]
---
.github/workflows/electron.yml | 91 +
.../@settings/tabs/debug/DebugTab.tsx | 4 +-
assets/entitlements.mac.plist | 25 +
assets/icons/icon.icns | Bin 0 -> 167243 bytes
assets/icons/icon.ico | Bin 0 -> 171717 bytes
assets/icons/icon.png | Bin 0 -> 18915 bytes
electron-builder.yml | 65 +
electron-update.yml | 4 +
electron/main/index.ts | 201 +
electron/main/tsconfig.json | 30 +
electron/main/ui/menu.ts | 29 +
electron/main/ui/window.ts | 51 +
electron/main/utils/auto-update.ts | 110 +
electron/main/utils/constants.ts | 4 +
electron/main/utils/cookie.ts | 40 +
electron/main/utils/reload.ts | 35 +
electron/main/utils/serve.ts | 71 +
electron/main/utils/store.ts | 3 +
electron/main/utils/vite-server.ts | 44 +
electron/main/vite.config.ts | 44 +
electron/preload/index.ts | 22 +
electron/preload/tsconfig.json | 7 +
electron/preload/vite.config.ts | 31 +
eslint.config.mjs | 2 +-
notarize.cjs | 31 +
package.json | 38 +-
pnpm-lock.yaml | 8036 +++++++++++------
tsconfig.json | 4 +-
vite-electron.config.ts | 75 +
29 files changed, 6215 insertions(+), 2882 deletions(-)
create mode 100644 .github/workflows/electron.yml
create mode 100644 assets/entitlements.mac.plist
create mode 100644 assets/icons/icon.icns
create mode 100644 assets/icons/icon.ico
create mode 100644 assets/icons/icon.png
create mode 100644 electron-builder.yml
create mode 100644 electron-update.yml
create mode 100644 electron/main/index.ts
create mode 100644 electron/main/tsconfig.json
create mode 100644 electron/main/ui/menu.ts
create mode 100644 electron/main/ui/window.ts
create mode 100644 electron/main/utils/auto-update.ts
create mode 100644 electron/main/utils/constants.ts
create mode 100644 electron/main/utils/cookie.ts
create mode 100644 electron/main/utils/reload.ts
create mode 100644 electron/main/utils/serve.ts
create mode 100644 electron/main/utils/store.ts
create mode 100644 electron/main/utils/vite-server.ts
create mode 100644 electron/main/vite.config.ts
create mode 100644 electron/preload/index.ts
create mode 100644 electron/preload/tsconfig.json
create mode 100644 electron/preload/vite.config.ts
create mode 100644 notarize.cjs
create mode 100644 vite-electron.config.ts
diff --git a/.github/workflows/electron.yml b/.github/workflows/electron.yml
new file mode 100644
index 00000000..29ff2a67
--- /dev/null
+++ b/.github/workflows/electron.yml
@@ -0,0 +1,91 @@
+name: Electron Build and Release
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - electron
+ tags:
+ - 'v*'
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ matrix:
+ os: [macos-latest, ubuntu-latest, windows-latest]
+ node-version: [18.18.0]
+ fail-fast: false
+
+ steps:
+ - name: Check out Git repository
+ uses: actions/checkout@v4
+
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+
+ - name: Install pnpm
+ uses: pnpm/action-setup@v2
+ with:
+ version: 9.14.4
+ run_install: false
+
+ - name: Get pnpm store directory
+ shell: bash
+ run: |
+ echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
+
+ - name: Setup pnpm cache
+ uses: actions/cache@v3
+ with:
+ path: ${{ env.STORE_PATH }}
+ key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
+ restore-keys: |
+ ${{ runner.os }}-pnpm-store-
+
+ - name: Install dependencies
+ run: pnpm install
+
+ # Install Linux dependencies
+ - name: Install Linux dependencies
+ if: matrix.os == 'ubuntu-latest'
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y rpm
+
+ # Build
+ - name: Build Electron app
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ CSC_LINK: ${{ secrets.CSC_LINK }}
+ CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
+ APPLE_ID: ${{ secrets.APPLE_ID }}
+ APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
+ APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+ NODE_OPTIONS: "--max_old_space_size=4096"
+ run: |
+ if [ "$RUNNER_OS" == "Windows" ]; then
+ pnpm run electron:build:win
+ elif [ "$RUNNER_OS" == "macOS" ]; then
+ pnpm run electron:build:mac
+ else
+ pnpm run electron:build:linux
+ fi
+ shell: bash
+
+ # Create Release
+ - name: Create Release
+ uses: softprops/action-gh-release@v2
+ with:
+ draft: ${{ github.ref_type == 'branch' }}
+ files: |
+ dist/*.exe
+ dist/*.dmg
+ dist/*.deb
+ dist/*.AppImage
+ dist/*.zip
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/app/components/@settings/tabs/debug/DebugTab.tsx b/app/components/@settings/tabs/debug/DebugTab.tsx
index e31ae473..25a86623 100644
--- a/app/components/@settings/tabs/debug/DebugTab.tsx
+++ b/app/components/@settings/tabs/debug/DebugTab.tsx
@@ -1353,7 +1353,9 @@ export default function DebugTab() {
- DOM Ready: {systemInfo ? (systemInfo.performance.timing.domReadyTime / 1000).toFixed(2) : '-'}s
+ DOM Ready: {systemInfo
+ ? (systemInfo.performance.timing.domReadyTime / 1000).toFixed(2)
+ : '-'}s
diff --git a/assets/entitlements.mac.plist b/assets/entitlements.mac.plist
new file mode 100644
index 00000000..0b2a95cb
--- /dev/null
+++ b/assets/entitlements.mac.plist
@@ -0,0 +1,25 @@
+
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+
+
+ com.apple.security.network.client
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/assets/icons/icon.icns b/assets/icons/icon.icns
new file mode 100644
index 0000000000000000000000000000000000000000..b3fbe2b676eb0da65e3789ddcc5c090fdafabb5a
GIT binary patch
literal 167243
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
dPNTxxg1PJWHF;)k$#^zy)L#m*Z4OQnUhTr
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