From db3cbad7942f7726b6e2d6d4516888aac42dcccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sudre?= Date: Tue, 12 Nov 2024 23:17:57 +0100 Subject: [PATCH] Squashed commit of the following: commit a081f8bec596989c2caa7a7665efbcc3514ae3e3 Merge: f1c5fbf 8c6e420 Author: Chris Mahoney Date: Tue Nov 12 12:41:58 2024 -0600 Merge pull request #213 from thecodacus/code-streaming feat(code-streaming): added code streaming to editor while AI is writing files commit 8c6e420546ac749f5ac71efced78b2bb873f06e6 Author: Anirban Kar Date: Wed Nov 13 00:04:41 2024 +0530 type fix commit 32699fd809e570088f8bac762a34c6e63bb479b2 Author: Anirban Kar Date: Wed Nov 13 00:03:33 2024 +0530 made types optional and, workbench get repo fix commit 5f46a18fba2be9f437a7946bcb5f6b51edc6d962 Author: Anirban Kar Date: Tue Nov 12 23:49:34 2024 +0530 recreated the lock file commit 6b3785958baaa150ae97ecd8b44d9b8b2e431499 Author: Anirban Kar Date: Tue Nov 12 23:47:27 2024 +0530 temporary removed lock file commit 58d95bdf7fc96b3f3dc619225f12be6cc1569d49 Author: Anirban Kar Date: Tue Nov 12 23:44:40 2024 +0530 chore: recreated the lock file commit a50945ee7f1227fd56916f8945a2a40c90e09e46 Author: Anirban Kar Date: Tue Nov 12 23:39:44 2024 +0530 chore: reverted pnpm lock commit cc43f06bd9d6da76131ed445e14886272fff7bba Author: Anirban Kar Date: Tue Nov 12 23:36:18 2024 +0530 chore: reverted pnpm package version to match ghaction commit 2d270e749b3cca346d19b4a30460c890a2a5572a Merge: 54351cd f1c5fbf Author: Anirban Kar Date: Tue Nov 12 23:20:30 2024 +0530 Merge branch 'main' into code-streaming commit f1c5fbf5b35fa3bb952969f50c2a1e12d9cc39ef Merge: 0203cf9 d7b1edc Author: Chris Mahoney Date: Tue Nov 12 10:46:30 2024 -0600 Merge pull request #261 from chrismahoney/fix/remove-ghaction-titlecheck Temporarily removing semantic-pr.yaml commit d7b1edc510a04fba24df82bfbafa637ef414c7e6 Author: Chris Mahoney Date: Tue Nov 12 10:34:51 2024 -0600 Temporarily removing semantic-pr.yaml in order to verify otherwise ready for review PRs. commit 0203cf95389684a8066425f3bd4967fe134cffce Merge: 1630d80 f28f7f0 Author: Chris Mahoney Date: Tue Nov 12 09:30:21 2024 -0600 Merge pull request #228 from thecodacus/feature--bolt-shell feat(bolt terminal): added dedicated bolt terminal, and attached to workbench commit 1630d80362db8069aa4aedd34c4e2b5baa5a751c Merge: 0b75051 b240344 Author: Cole Medin Date: Mon Nov 11 18:40:30 2024 -0600 Merge pull request #247 from JNN5/main fix: adds missing -t for dockerbuild:prod command in package.json commit 0b75051ba560ff2d867752dc47094ba1fdce73c6 Merge: 4b492b9 9c84880 Author: Cole Medin Date: Mon Nov 11 18:39:48 2024 -0600 Merge pull request #254 from ali00209/new_bolt5 fix: bug #245 commit 9c848802924f4820d063504663104b952f4559fb Author: ali00209 Date: Tue Nov 12 05:10:54 2024 +0500 fix: bug #245 commit b240344e7be95c614a7151689587258d7d2f2e05 Author: Jonas Neumann Date: Mon Nov 11 17:41:15 2024 +0800 fix: adds missing -t for dockerbuild:prod command in package.json commit 4b492b9d974bc134518871da42c86a634c94a886 Merge: c968948 32ae66a Author: Eduard Ruzga Date: Mon Nov 11 09:58:05 2024 +0200 Merge pull request #104 from karrot0/main feat: lm studio integration commit 32ae66a8e599b1f9107fae85f2a87b92357cefd3 Merge: dff340e 6f8001a Author: Karrot0 Date: Sun Nov 10 21:08:06 2024 -0500 Merge branch 'main' of https://github.com/karrot0/bolt.new-any-llm commit dff340eba56797cb734db2fceb7fb57207f50b8e Author: Karrot0 Date: Sun Nov 10 21:08:01 2024 -0500 @wonderwhy-er suggestion fix pr commit 6f8001aeaf3c096bf48c1aca7cee93415425b2e8 Merge: 9a7d28a c968948 Author: Karrot <86092166+karrot0@users.noreply.github.com> Date: Sun Nov 10 21:04:29 2024 -0500 Merge branch 'coleam00:main' into main commit c9689488453ef630ab39c67dfbb43b4dace895c4 Merge: 848c697 93ff797 Author: Chris Mahoney Date: Sun Nov 10 17:16:10 2024 -0600 Merge pull request #242 from aaronbolton/main removing GitHub action workflow github-build-push.yml commit 93ff797427f681bd57871f2b37dd1a1294f5af7f Merge: 1b44a0b 848c697 Author: Aaron Bolton Date: Sun Nov 10 20:24:42 2024 +0000 Merge branch 'main' of https://github.com/aaronbolton/bolt.new-any-llm commit 1b44a0b0d7d9a2eac59c9a06086cb11db1fde1d3 Author: Aaron Bolton Date: Sun Nov 10 20:24:32 2024 +0000 Delete github-build-push.yml commit 848c697d09223c823a4befbdc4c00309a427259f Author: Cole Medin Date: Sun Nov 10 13:43:28 2024 -0600 Update README.md commit 52ae333e8615a3320611189ce9a8489c55c99b7f Merge: ce6b65e c35211f Author: Eduard Ruzga Date: Sun Nov 10 15:06:51 2024 +0200 Merge pull request #205 from milutinke/claude-new-sonnet-and-haiku feat: added the latest Sonnet 3.5 and Haiku 3.5 commit c35211f4a075763eaa8da54baa362900be1d61fb Merge: bb0546d ce6b65e Author: Anon Date: Sun Nov 10 13:58:34 2024 +0100 Merge branch 'main' into claude-new-sonnet-and-haiku commit 9a7d28a97d7891b73ff318094f6d4b7fcbf46a78 Merge: f9e750d ce6b65e Author: Eduard Ruzga Date: Sun Nov 10 14:55:17 2024 +0200 Merge branch 'main' into main commit ce6b65e66f60c38e08eb50aecd37ebfde08baa2c Merge: 13b1321 435d6b4 Author: Chris Mahoney Date: Sat Nov 9 20:53:00 2024 -0600 Merge pull request #178 from albahrani/patch-1 docs: update README.md changed .env to .env.local commit 13b1321460873b044a38aefedd3a7980dca3de99 Author: Cole Medin Date: Sat Nov 9 07:59:06 2024 -0600 Noting that API key will still work if set in .env file commit b3fe2076a7fb8578907178b2359d1f132716f0d5 Author: Cole Medin Date: Sat Nov 9 07:56:43 2024 -0600 Fixing merge conflicts in BaseChat.tsx commit 936a9c0f69db47b35165cf6a807fd5771f82ea8c Merge: d18517a f30612d Author: Cole Medin Date: Sat Nov 9 07:45:06 2024 -0600 Merge pull request #188 from TommyHolmberg/respect-provider-choice fix: respect provider choice from UI commit f30612dcfd6b979280e5dc3a7c36e41eef4fb739 Merge: 9b97837 d18517a Author: Cole Medin Date: Sat Nov 9 07:44:40 2024 -0600 Merge branch 'main' into respect-provider-choice commit f28f7f0715497dfbbca62524fa42c2a3229b4ab4 Author: Anirban Kar Date: Sat Nov 9 14:23:29 2024 +0530 chore: cleanup logging commit d4c4fe1e5c16d6ecaf285df9cd9c798fec6319be Author: Anirban Kar Date: Sat Nov 9 13:43:19 2024 +0530 feat: hyperlinked on "Start application" actionto switch to preview in workbench commit 719384cfbdb1e5a3684874e22ab45c1d6d5a442b Author: Anirban Kar Date: Sat Nov 9 12:59:42 2024 +0530 feat(bolt-terminal) bolt terminal integrated with the system commit d18517a08d77c0588e45d63ac9b16cf91ad2efb6 Merge: 1ba0606 52cd1ae Author: Chris Mahoney Date: Fri Nov 8 23:22:45 2024 -0600 Merge pull request #101 from ali00209/new_bolt1 feat: add ability to enter API keys in the UI commit 52cd1aea9df92316b5ae9df74b31bc82214e88d6 Merge: 73a07c9 1ba0606 Author: Ali <133174663+ali00209@users.noreply.github.com> Date: Sat Nov 9 09:59:21 2024 +0500 Merge branch 'main' into new_bolt1 commit 73a07c93e41f1547cee26c1e6e8c1dfa33613c7d Author: ali00209 Date: Sat Nov 9 09:55:40 2024 +0500 fix: Resolved commit f9e750d76e79c05720a12905ef4d44f5fe847bd8 Merge: 73505a3 1ba0606 Author: Karrot <86092166+karrot0@users.noreply.github.com> Date: Fri Nov 8 17:10:40 2024 -0500 Merge branch 'main' into main commit d1f3e8cbecc4e3704f382e93a4b2c6195678cfba Author: Anirban Kar Date: Fri Nov 8 21:47:31 2024 +0530 feat: added bolt dedicated shell commit 9b97837bb29013b846562fb538185fbf3f753c77 Author: Tommy Date: Fri Nov 8 08:51:22 2024 +0100 Show which model name and provider is used in user message. commit 54351cd845d7982b6a781b3eed82127f5291539b Author: Anirban Kar Date: Fri Nov 8 11:14:15 2024 +0530 feature(code-streaming): added code streaming to editor while AI is writing code commit 1ba0606e58d34ef787cb5b16139f2c5bc1dce164 Merge: 775283e dec6513 Author: Cole Medin Date: Thu Nov 7 18:12:27 2024 -0600 Merge pull request #209 from patrykwegrzyn/main feat: set numCtx = 32768 for Ollama models commit dec65132e74ed4625545da4678fab1ee5da1ffec Author: Patryk Wegrzyn Date: Thu Nov 7 23:51:12 2024 +0000 Set numCtx = 32768 for Ollama models commit bb0546d85ada1385499171655df63ad22a89c9a6 Author: Anon Date: Thu Nov 7 14:38:22 2024 +0100 Added the latest Sonnet 3.5 and Haiku 3.5 commit 775283e3b48c9c0cfcd0303fa65af3f19532633f Merge: a6d81b1 8d7f108 Author: Cole Medin Date: Thu Nov 7 06:29:01 2024 -0600 Merge pull request #196 from milutinke/x-ai feat: added support for xAI Grok Beta commit 8d7f108dfa3700d8a7444962a2286380bb44dd7a Author: Anon Date: Thu Nov 7 01:14:27 2024 +0100 Added the XAI_API_KEY variable to the .env.example commit 9a2fc9220d252d20f046074e70abc47976e67216 Author: Anon Date: Thu Nov 7 01:03:37 2024 +0100 Added support for xAI Grok Beta commit 73505a32c7167aa4a22ec8da04f8981b695a41ca Merge: 27b6684 a6d81b1 Author: Karrot <86092166+karrot0@users.noreply.github.com> Date: Wed Nov 6 15:52:24 2024 -0500 Merge branch 'coleam00:main' into main commit 2a362b9e0b4e04654607325b63eaedc10f4a64a4 Author: Tommy Date: Wed Nov 6 21:35:54 2024 +0100 Added sanitization for user messages. Use regex defined in constants.ts instead of redefining. commit 074e2f3016706a87ea72b7f58c720bf9f4f4f1d8 Author: Tommy Date: Wed Nov 6 11:10:08 2024 +0100 Moved provider and setProvider variables to the higher level component so that it can be accessed in sendMessage. Added provider to message queue in sendMessage. Changed streamText to extract both model and provider. commit 435d6b4d5813af81a0ecddc282afd71f95a714f6 Author: Alexander Al-Bahrani Date: Tue Nov 5 12:41:58 2024 +0100 Update README.md changed .env to .env.local The installation description stated to create an .env file out of the .env.example This leads to an error when executing docker compose up since the docker-compose.yml expects the environment file being called .env.local commit 27b6684a5099ab34e67eb955bad0e7fd1a75d20f Merge: 449d0a3 e7ce257 Author: Karrot <86092166+karrot0@users.noreply.github.com> Date: Sun Nov 3 16:21:16 2024 -0500 Merge branch 'coleam00:main' into main commit 449d0a311fcf93e99308696158f8b4bb80e7fa42 Merge: 8dfc4f7 065be0f Author: Karrot <86092166+karrot0@users.noreply.github.com> Date: Wed Oct 30 14:52:23 2024 -0400 Merge branch 'coleam00:main' into main commit 8dfc4f7ba911cfb76351fdb3fe6bfbd950ee7737 Author: Karrot <86092166+karrot0@users.noreply.github.com> Date: Wed Oct 30 13:05:57 2024 -0400 Changed mode.ts to add BaseURL. Thanks @alumbs commit 748e0d64ef0036a4ca124a11e8055b6f2e1095aa Author: Karrot <86092166+karrot0@users.noreply.github.com> Date: Sun Oct 27 23:26:54 2024 -0400 Remove Package-lock.json commit 4edcc5e3311f3f999eea833d27b7be0ada071a2b Author: Karrot0 Date: Sun Oct 27 23:16:07 2024 -0400 LM Studio Integration --- .env.example | 11 ++ .github/workflows/github-build-push.yml | 39 ------ .github/workflows/semantic-pr.yaml | 32 ----- .gitignore | 1 + README.md | 9 +- app/components/chat/APIKeyManager.tsx | 2 +- app/components/chat/Artifact.tsx | 20 ++- app/components/chat/BaseChat.tsx | 3 +- app/components/chat/Chat.client.tsx | 30 ++++- app/components/chat/UserMessage.tsx | 4 +- app/components/workbench/EditorPanel.tsx | 74 +++++++++--- app/lib/.server/llm/api-key.ts | 4 + app/lib/.server/llm/model.ts | 27 ++++- app/lib/.server/llm/prompts.ts | 12 +- app/lib/.server/llm/stream-text.ts | 3 - app/lib/hooks/useMessageParser.ts | 4 + app/lib/hooks/usePromptEnhancer.ts | 48 +++++--- app/lib/runtime/action-runner.ts | 80 +++++++----- app/lib/runtime/message-parser.ts | 20 ++- app/lib/stores/terminal.ts | 17 ++- app/lib/stores/workbench.ts | 72 ++++++++--- app/routes/api.enhancer.ts | 40 ++++-- app/types/actions.ts | 6 +- app/types/terminal.ts | 1 + app/utils/constants.ts | 7 +- app/utils/shell.ts | 148 ++++++++++++++++++++++- package.json | 4 +- vite.config.ts | 2 +- worker-configuration.d.ts | 1 + 29 files changed, 520 insertions(+), 201 deletions(-) delete mode 100644 .github/workflows/github-build-push.yml delete mode 100644 .github/workflows/semantic-pr.yaml diff --git a/.env.example b/.env.example index ec825e8ee..46a21e892 100644 --- a/.env.example +++ b/.env.example @@ -43,5 +43,16 @@ OPENAI_LIKE_API_KEY= # You only need this environment variable set if you want to use Mistral models MISTRAL_API_KEY= + +# Get LMStudio Base URL from LM Studio Developer Console +# Make sure to enable CORS +# Example: http://localhost:1234 +LMSTUDIO_API_BASE_URL= + +# Get your xAI API key +# https://x.ai/api +# You only need this environment variable set if you want to use xAI models +XAI_API_KEY= + # Include this environment variable if you want more logging for debugging locally VITE_LOG_LEVEL=debug diff --git a/.github/workflows/github-build-push.yml b/.github/workflows/github-build-push.yml deleted file mode 100644 index 4d4db05d8..000000000 --- a/.github/workflows/github-build-push.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: Build and Push Container - -on: - push: - branches: - - main - # paths: - # - 'Dockerfile' - workflow_dispatch: -jobs: - build-and-push: - runs-on: [ubuntu-latest] - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v1 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and Push Containers - uses: docker/build-push-action@v2 - with: - context: . - file: Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: | - ghcr.io/${{ github.repository }}:latest - ghcr.io/${{ github.repository }}:${{ github.sha }} diff --git a/.github/workflows/semantic-pr.yaml b/.github/workflows/semantic-pr.yaml deleted file mode 100644 index 503b04552..000000000 --- a/.github/workflows/semantic-pr.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: Semantic Pull Request -on: - pull_request_target: - types: [opened, reopened, edited, synchronize] -permissions: - pull-requests: read -jobs: - main: - name: Validate PR Title - runs-on: ubuntu-latest - steps: - # https://github.com/amannn/action-semantic-pull-request/releases/tag/v5.5.3 - - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - subjectPattern: ^(?![A-Z]).+$ - subjectPatternError: | - The subject "{subject}" found in the pull request title "{title}" - didn't match the configured pattern. Please ensure that the subject - doesn't start with an uppercase character. - types: | - fix - feat - chore - build - ci - perf - docs - refactor - revert - test diff --git a/.gitignore b/.gitignore index 69d279030..b43105b77 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ dist-ssr _worker.bundle Modelfile +modelfiles diff --git a/README.md b/README.md index fb70e7566..54ae824ed 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,9 @@ This fork of Bolt.new allows you to choose the LLM that you use for each prompt! - ✅ Ability to sync files (one way sync) to local folder (@muzafferkadir) - ✅ Containerize the application with Docker for easy installation (@aaronbolton) - ✅ Publish projects directly to GitHub (@goncaloalves) -- ⬜ Prevent Bolt from rewriting files as often (Done but need to review PR still) +- ✅ Ability to enter API keys in the UI (@ali00209) +- ✅ xAI Grok Beta Integration (@milutinke) +- ⬜ **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** Load local projects into the app - ⬜ **HIGH PRIORITY** - Attach images to prompts @@ -34,7 +36,6 @@ This fork of Bolt.new allows you to choose the LLM that you use for each prompt! - ⬜ Ability to revert code to earlier version - ⬜ Prompt caching - ⬜ Better prompt enhancing -- ⬜ Ability to enter API keys in the UI - ⬜ 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. @@ -85,7 +86,7 @@ If you see usr/local/bin in the output then you're good to go. git clone https://github.com/coleam00/bolt.new-any-llm.git ``` -3. Rename .env.example to .env and add your LLM API keys. You will find this file on a Mac at "[your name]/bold.new-any-llm/.env.example". For Windows and Linux the path will be similar. +3. Rename .env.example to .env.local and add your LLM API keys. You will find this file on a Mac at "[your name]/bold.new-any-llm/.env.example". For Windows and Linux the path will be similar. ![image](https://github.com/user-attachments/assets/7e6a532c-2268-401f-8310-e8d20c731328) @@ -115,7 +116,7 @@ Optionally, you can set the debug level: VITE_LOG_LEVEL=debug ``` -**Important**: Never commit your `.env` file to version control. It's already included in .gitignore. +**Important**: Never commit your `.env.local` file to version control. It's already included in .gitignore. ## Run with Docker diff --git a/app/components/chat/APIKeyManager.tsx b/app/components/chat/APIKeyManager.tsx index aab70107c..a35724c8c 100644 --- a/app/components/chat/APIKeyManager.tsx +++ b/app/components/chat/APIKeyManager.tsx @@ -37,7 +37,7 @@ export const APIKeyManager: React.FC = ({ provider, apiKey, ) : ( <> - {apiKey ? '••••••••' : 'Not set'} + {apiKey ? '••••••••' : 'Not set (will still work if set in .env file)'} setIsEditing(true)} title="Edit API Key">
diff --git a/app/components/chat/Artifact.tsx b/app/components/chat/Artifact.tsx index 9de52dd94..62020fd84 100644 --- a/app/components/chat/Artifact.tsx +++ b/app/components/chat/Artifact.tsx @@ -151,7 +151,13 @@ const ActionList = memo(({ actions }: ActionListProps) => {
{status === 'running' ? ( -
+ <> + {type !== 'start' ? ( +
+ ) : ( +
+ )} + ) : status === 'pending' ? (
) : status === 'complete' ? ( @@ -171,9 +177,19 @@ const ActionList = memo(({ actions }: ActionListProps) => {
Run command
+ ) : type === 'start' ? ( + { + e.preventDefault(); + workbenchStore.currentView.set('preview'); + }} + className="flex items-center w-full min-h-[28px]" + > + Start Application + ) : null}
- {type === 'shell' && ( + {(type === 'shell' || type === 'start') && ( ( input = '', model, setModel, + provider, + setProvider, sendMessage, handleInputChange, enhancePrompt, @@ -151,7 +153,6 @@ export const BaseChat = React.forwardRef( ref, ) => { const TEXTAREA_MAX_HEIGHT = chatStarted ? 400 : 200; - const [provider, setProvider] = useState(DEFAULT_PROVIDER); const [apiKeys, setApiKeys] = useState>({}); const [modelList, setModelList] = useState([]); // État pour la liste des modèles diff --git a/app/components/chat/Chat.client.tsx b/app/components/chat/Chat.client.tsx index c200fc486..534b8188f 100644 --- a/app/components/chat/Chat.client.tsx +++ b/app/components/chat/Chat.client.tsx @@ -11,7 +11,7 @@ import { useChatHistory } from '~/lib/persistence'; import { chatStore } from '~/lib/stores/chat'; import { workbenchStore } from '~/lib/stores/workbench'; import { fileModificationsToHTML } from '~/utils/diff'; -import { DEFAULT_MODEL } from '~/utils/constants'; +import { DEFAULT_MODEL, DEFAULT_PROVIDER } from '~/utils/constants'; import { cubicEasingFn } from '~/utils/easings'; import { createScopedLogger, renderLogger } from '~/utils/logger'; import { BaseChat } from './BaseChat'; @@ -216,6 +216,16 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp } }, []); + const handleModelChange = (newModel: string) => { + setModel(newModel); + Cookies.set('selectedModel', newModel, { expires: 30 }); + }; + + const handleProviderChange = (newProvider: string) => { + setProvider(newProvider); + Cookies.set('selectedProvider', newProvider, { expires: 30 }); + }; + return ( { - enhancePrompt(input, (input) => { - setInput(input); - scrollTextArea(); - }); + enhancePrompt( + input, + (input) => { + setInput(input); + scrollTextArea(); + }, + model, + provider, + apiKeys, + ); }} /> ); diff --git a/app/components/chat/UserMessage.tsx b/app/components/chat/UserMessage.tsx index 62e054b21..803d2cdab 100644 --- a/app/components/chat/UserMessage.tsx +++ b/app/components/chat/UserMessage.tsx @@ -1,7 +1,7 @@ // @ts-nocheck // Preventing TS checks with files presented in the video for a better presentation. import { modificationsRegex } from '~/utils/diff'; -import { MODEL_REGEX } from '~/utils/constants'; +import { MODEL_REGEX, PROVIDER_REGEX } from '~/utils/constants'; import { Markdown } from './Markdown'; interface UserMessageProps { @@ -17,5 +17,5 @@ export function UserMessage({ content }: UserMessageProps) { } function sanitizeUserMessage(content: string) { - return content.replace(modificationsRegex, '').replace(MODEL_REGEX, '').trim(); + return content.replace(modificationsRegex, '').replace(MODEL_REGEX, 'Using: $1').replace(PROVIDER_REGEX, ' ($1)\n\n').trim(); } diff --git a/app/components/workbench/EditorPanel.tsx b/app/components/workbench/EditorPanel.tsx index d1a265a66..e789f1d6d 100644 --- a/app/components/workbench/EditorPanel.tsx +++ b/app/components/workbench/EditorPanel.tsx @@ -18,7 +18,7 @@ import { themeStore } from '~/lib/stores/theme'; import { workbenchStore } from '~/lib/stores/workbench'; import { classNames } from '~/utils/classNames'; import { WORK_DIR } from '~/utils/constants'; -import { renderLogger } from '~/utils/logger'; +import { logger, renderLogger } from '~/utils/logger'; import { isMobile } from '~/utils/mobile'; import { FileBreadcrumb } from './FileBreadcrumb'; import { FileTree } from './FileTree'; @@ -199,25 +199,48 @@ export const EditorPanel = memo(
- {Array.from({ length: terminalCount }, (_, index) => { + {Array.from({ length: terminalCount + 1 }, (_, index) => { const isActive = activeTerminal === index; return ( - + ) : ( + <> + + )} - onClick={() => setActiveTerminal(index)} - > -
- Terminal {terminalCount > 1 && index + 1} - + ); })} {terminalCount < MAX_TERMINALS && } @@ -229,9 +252,26 @@ export const EditorPanel = memo( onClick={() => workbenchStore.toggleTerminal(false)} />
- {Array.from({ length: terminalCount }, (_, index) => { + {Array.from({ length: terminalCount + 1 }, (_, index) => { const isActive = activeTerminal === index; + if (index == 0) { + logger.info('Starting bolt terminal'); + return ( + { + terminalRefs.current.push(ref); + }} + onTerminalReady={(terminal) => workbenchStore.attachBoltTerminal(terminal)} + onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)} + theme={theme} + /> + ); + } return ( ): ModelInfo { const apiKey = getAPIKey(env, provider, apiKeys); const baseURL = getBaseURL(env, provider); @@ -101,6 +122,10 @@ export function getModel(provider: string, model: string, env: Env, apiKeys?: Re return getDeepseekModel(apiKey, model); case 'Mistral': return getMistralModel(apiKey, model); + case 'LMStudio': + return getLMStudioModel(baseURL, model); + case 'xAI': + return getXAIModel(apiKey, model); default: return getOllamaModel(baseURL, model); } diff --git a/app/lib/.server/llm/prompts.ts b/app/lib/.server/llm/prompts.ts index 28c0e6933..30ecc5878 100644 --- a/app/lib/.server/llm/prompts.ts +++ b/app/lib/.server/llm/prompts.ts @@ -202,10 +202,16 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w - When Using \`npx\`, ALWAYS provide the \`--yes\` flag. - When running multiple shell commands, use \`&&\` to run them sequentially. - - ULTRA IMPORTANT: Do NOT re-run a dev command if there is one that starts a dev server and new dependencies were installed or files updated! If a dev server has started already, assume that installing dependencies will be executed in a different process and will be picked up by the dev server. + - ULTRA IMPORTANT: Do NOT re-run a dev command with shell action use dev action to run dev commands - file: For writing new files or updating existing files. For each file add a \`filePath\` attribute to the opening \`\` tag to specify the file path. The content of the file artifact is the file contents. All file paths MUST BE relative to the current working directory. + - start: For starting development server. + - Use to start application if not already started or NEW dependencies added + - Only use this action when you need to run a dev server or start the application + - ULTRA IMORTANT: do NOT re-run a dev server if files updated, existing dev server can autometically detect changes and executes the file changes + + 9. The order of the actions is VERY IMPORTANT. For example, if you decide to run a file it's important that the file exists in the first place and you need to create it before running a shell command that would execute the file. 10. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \`package.json\` then you should create that first! @@ -305,7 +311,7 @@ Here are some examples of correct usage of artifacts: ... - + npm run dev @@ -362,7 +368,7 @@ Here are some examples of correct usage of artifacts: ... - + npm run dev diff --git a/app/lib/.server/llm/stream-text.ts b/app/lib/.server/llm/stream-text.ts index 7a364ffc6..72e758098 100644 --- a/app/lib/.server/llm/stream-text.ts +++ b/app/lib/.server/llm/stream-text.ts @@ -63,9 +63,6 @@ export function streamText( model: getModel(model.provider, model.name, env, apiKeys), system: getSystemPrompt(), maxTokens: MAX_TOKENS, - // headers: { - // 'anthropic-beta': 'max-tokens-3-5-sonnet-2024-07-15', - // }, messages: convertToCoreMessages(messages), ...options, }); diff --git a/app/lib/hooks/useMessageParser.ts b/app/lib/hooks/useMessageParser.ts index a70fb82f4..97a063da5 100644 --- a/app/lib/hooks/useMessageParser.ts +++ b/app/lib/hooks/useMessageParser.ts @@ -36,6 +36,10 @@ const messageParser = new StreamingMessageParser({ workbenchStore.runAction(data); }, + onActionStream: (data) => { + logger.trace('onActionStream', data.action); + workbenchStore.runAction(data, true); + }, }, }); diff --git a/app/lib/hooks/usePromptEnhancer.ts b/app/lib/hooks/usePromptEnhancer.ts index f376cc0cd..ee4499920 100644 --- a/app/lib/hooks/usePromptEnhancer.ts +++ b/app/lib/hooks/usePromptEnhancer.ts @@ -12,41 +12,55 @@ export function usePromptEnhancer() { setPromptEnhanced(false); }; - const enhancePrompt = async (input: string, setInput: (value: string) => void) => { + const enhancePrompt = async ( + input: string, + setInput: (value: string) => void, + model: string, + provider: string, + apiKeys?: Record + ) => { setEnhancingPrompt(true); setPromptEnhanced(false); - + + const requestBody: any = { + message: input, + model, + provider, + }; + + if (apiKeys) { + requestBody.apiKeys = apiKeys; + } + const response = await fetch('/api/enhancer', { method: 'POST', - body: JSON.stringify({ - message: input, - }), + body: JSON.stringify(requestBody), }); - + const reader = response.body?.getReader(); - + const originalInput = input; - + if (reader) { const decoder = new TextDecoder(); - + let _input = ''; let _error; - + try { setInput(''); - + while (true) { const { value, done } = await reader.read(); - + if (done) { break; } - + _input += decoder.decode(value); - + logger.trace('Set input', _input); - + setInput(_input); } } catch (error) { @@ -56,10 +70,10 @@ export function usePromptEnhancer() { if (_error) { logger.error(_error); } - + setEnhancingPrompt(false); setPromptEnhanced(true); - + setTimeout(() => { setInput(_input); }); diff --git a/app/lib/runtime/action-runner.ts b/app/lib/runtime/action-runner.ts index e2ea6a226..f94390be9 100644 --- a/app/lib/runtime/action-runner.ts +++ b/app/lib/runtime/action-runner.ts @@ -1,10 +1,12 @@ -import { WebContainer } from '@webcontainer/api'; -import { map, type MapStore } from 'nanostores'; +import { WebContainer, type WebContainerProcess } from '@webcontainer/api'; +import { atom, map, type MapStore } from 'nanostores'; import * as nodePath from 'node:path'; import type { BoltAction } from '~/types/actions'; import { createScopedLogger } from '~/utils/logger'; import { unreachable } from '~/utils/unreachable'; import type { ActionCallbackData } from './message-parser'; +import type { ITerminal } from '~/types/terminal'; +import type { BoltShell } from '~/utils/shell'; const logger = createScopedLogger('ActionRunner'); @@ -36,11 +38,14 @@ type ActionsMap = MapStore>; export class ActionRunner { #webcontainer: Promise; #currentExecutionPromise: Promise = Promise.resolve(); - + #shellTerminal: () => BoltShell; + runnerId = atom(`${Date.now()}`); actions: ActionsMap = map({}); - constructor(webcontainerPromise: Promise) { + constructor(webcontainerPromise: Promise, getShellTerminal: () => BoltShell) { this.#webcontainer = webcontainerPromise; + this.#shellTerminal = getShellTerminal; + } addAction(data: ActionCallbackData) { @@ -72,7 +77,7 @@ export class ActionRunner { }); } - async runAction(data: ActionCallbackData) { + async runAction(data: ActionCallbackData, isStreaming: boolean = false) { const { actionId } = data; const action = this.actions.get()[actionId]; @@ -83,19 +88,22 @@ export class ActionRunner { if (action.executed) { return; } + if (isStreaming && action.type !== 'file') { + return; + } - this.#updateAction(actionId, { ...action, ...data.action, executed: true }); + this.#updateAction(actionId, { ...action, ...data.action, executed: !isStreaming }); this.#currentExecutionPromise = this.#currentExecutionPromise .then(() => { - return this.#executeAction(actionId); + return this.#executeAction(actionId, isStreaming); }) .catch((error) => { console.error('Action failed:', error); }); } - async #executeAction(actionId: string) { + async #executeAction(actionId: string, isStreaming: boolean = false) { const action = this.actions.get()[actionId]; this.#updateAction(actionId, { status: 'running' }); @@ -110,11 +118,16 @@ export class ActionRunner { await this.#runFileAction(action); break; } + case 'start': { + await this.#runStartAction(action) + break; + } } - this.#updateAction(actionId, { status: action.abortSignal.aborted ? 'aborted' : 'complete' }); + this.#updateAction(actionId, { status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete' }); } catch (error) { this.#updateAction(actionId, { status: 'failed', error: 'Action failed' }); + logger.error(`[${action.type}]:Action failed\n\n`, error); // re-throw the error to be caught in the promise chain throw error; @@ -125,28 +138,38 @@ export class ActionRunner { if (action.type !== 'shell') { unreachable('Expected shell action'); } + const shell = this.#shellTerminal() + await shell.ready() + if (!shell || !shell.terminal || !shell.process) { + unreachable('Shell terminal not found'); + } + const resp = await shell.executeCommand(this.runnerId.get(), action.content) + logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`) + if (resp?.exitCode != 0) { + throw new Error("Failed To Execute Shell Command"); - const webcontainer = await this.#webcontainer; - - const process = await webcontainer.spawn('jsh', ['-c', action.content], { - env: { npm_config_yes: true }, - }); - - action.abortSignal.addEventListener('abort', () => { - process.kill(); - }); - - process.output.pipeTo( - new WritableStream({ - write(data) { - console.log(data); - }, - }), - ); + } + } - const exitCode = await process.exit; + async #runStartAction(action: ActionState) { + if (action.type !== 'start') { + unreachable('Expected shell action'); + } + if (!this.#shellTerminal) { + unreachable('Shell terminal not found'); + } + const shell = this.#shellTerminal() + await shell.ready() + if (!shell || !shell.terminal || !shell.process) { + unreachable('Shell terminal not found'); + } + const resp = await shell.executeCommand(this.runnerId.get(), action.content) + logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`) - logger.debug(`Process terminated with code ${exitCode}`); + if (resp?.exitCode != 0) { + throw new Error("Failed To Start Application"); + } + return resp } async #runFileAction(action: ActionState) { @@ -177,7 +200,6 @@ export class ActionRunner { logger.error('Failed to write file\n\n', error); } } - #updateAction(id: string, newState: ActionStateUpdate) { const actions = this.actions.get(); diff --git a/app/lib/runtime/message-parser.ts b/app/lib/runtime/message-parser.ts index 317f81dff..4b564da16 100644 --- a/app/lib/runtime/message-parser.ts +++ b/app/lib/runtime/message-parser.ts @@ -28,6 +28,7 @@ export interface ParserCallbacks { onArtifactOpen?: ArtifactCallback; onArtifactClose?: ArtifactCallback; onActionOpen?: ActionCallback; + onActionStream?: ActionCallback; onActionClose?: ActionCallback; } @@ -54,7 +55,7 @@ interface MessageState { export class StreamingMessageParser { #messages = new Map(); - constructor(private _options: StreamingMessageParserOptions = {}) {} + constructor(private _options: StreamingMessageParserOptions = {}) { } parse(messageId: string, input: string) { let state = this.#messages.get(messageId); @@ -118,6 +119,21 @@ export class StreamingMessageParser { i = closeIndex + ARTIFACT_ACTION_TAG_CLOSE.length; } else { + if ('type' in currentAction && currentAction.type === 'file') { + let content = input.slice(i); + + this._options.callbacks?.onActionStream?.({ + artifactId: currentArtifact.id, + messageId, + actionId: String(state.actionId - 1), + action: { + ...currentAction as FileAction, + content, + filePath: currentAction.filePath, + }, + + }); + } break; } } else { @@ -256,7 +272,7 @@ export class StreamingMessageParser { } (actionAttributes as FileAction).filePath = filePath; - } else if (actionType !== 'shell') { + } else if (!(['shell', 'start'].includes(actionType))) { logger.warn(`Unknown action type '${actionType}'`); } diff --git a/app/lib/stores/terminal.ts b/app/lib/stores/terminal.ts index 419320e3a..b2537ccb4 100644 --- a/app/lib/stores/terminal.ts +++ b/app/lib/stores/terminal.ts @@ -1,14 +1,15 @@ import type { WebContainer, WebContainerProcess } from '@webcontainer/api'; import { atom, type WritableAtom } from 'nanostores'; import type { ITerminal } from '~/types/terminal'; -import { newShellProcess } from '~/utils/shell'; +import { newBoltShellProcess, newShellProcess } from '~/utils/shell'; import { coloredText } from '~/utils/terminal'; export class TerminalStore { #webcontainer: Promise; #terminals: Array<{ terminal: ITerminal; process: WebContainerProcess }> = []; + #boltTerminal = newBoltShellProcess() - showTerminal: WritableAtom = import.meta.hot?.data.showTerminal ?? atom(false); + showTerminal: WritableAtom = import.meta.hot?.data.showTerminal ?? atom(true); constructor(webcontainerPromise: Promise) { this.#webcontainer = webcontainerPromise; @@ -17,10 +18,22 @@ export class TerminalStore { import.meta.hot.data.showTerminal = this.showTerminal; } } + get boltTerminal() { + return this.#boltTerminal; + } toggleTerminal(value?: boolean) { this.showTerminal.set(value !== undefined ? value : !this.showTerminal.get()); } + async attachBoltTerminal(terminal: ITerminal) { + try { + let wc = await this.#webcontainer + await this.#boltTerminal.init(wc, terminal) + } catch (error: any) { + terminal.write(coloredText.red('Failed to spawn bolt shell\n\n') + error.message); + return; + } + } async attachTerminal(terminal: ITerminal) { try { diff --git a/app/lib/stores/workbench.ts b/app/lib/stores/workbench.ts index c42cc6275..8589391c8 100644 --- a/app/lib/stores/workbench.ts +++ b/app/lib/stores/workbench.ts @@ -11,7 +11,9 @@ import { PreviewsStore } from './previews'; import { TerminalStore } from './terminal'; import JSZip from 'jszip'; import { saveAs } from 'file-saver'; -import { Octokit } from "@octokit/rest"; +import { Octokit, type RestEndpointMethodTypes } from "@octokit/rest"; +import * as nodePath from 'node:path'; +import type { WebContainerProcess } from '@webcontainer/api'; export interface ArtifactState { id: string; @@ -39,6 +41,7 @@ export class WorkbenchStore { unsavedFiles: WritableAtom> = import.meta.hot?.data.unsavedFiles ?? atom(new Set()); modifiedFiles = new Set(); artifactIdList: string[] = []; + #boltTerminal: { terminal: ITerminal; process: WebContainerProcess } | undefined; constructor() { if (import.meta.hot) { @@ -76,6 +79,9 @@ export class WorkbenchStore { get showTerminal() { return this.#terminalStore.showTerminal; } + get boltTerminal() { + return this.#terminalStore.boltTerminal; + } toggleTerminal(value?: boolean) { this.#terminalStore.toggleTerminal(value); @@ -84,6 +90,10 @@ export class WorkbenchStore { attachTerminal(terminal: ITerminal) { this.#terminalStore.attachTerminal(terminal); } + attachBoltTerminal(terminal: ITerminal) { + + this.#terminalStore.attachBoltTerminal(terminal); + } onTerminalResize(cols: number, rows: number) { this.#terminalStore.onTerminalResize(cols, rows); @@ -232,7 +242,7 @@ export class WorkbenchStore { id, title, closed: false, - runner: new ActionRunner(webcontainer), + runner: new ActionRunner(webcontainer, () => this.boltTerminal), }); } @@ -258,7 +268,7 @@ export class WorkbenchStore { artifact.runner.addAction(data); } - async runAction(data: ActionCallbackData) { + async runAction(data: ActionCallbackData, isStreaming: boolean = false) { const { messageId } = data; const artifact = this.#getArtifact(messageId); @@ -266,8 +276,29 @@ export class WorkbenchStore { if (!artifact) { unreachable('Artifact not found'); } + if (data.action.type === 'file') { + let wc = await webcontainer + const fullPath = nodePath.join(wc.workdir, data.action.filePath); + if (this.selectedFile.value !== fullPath) { + this.setSelectedFile(fullPath); + } + if (this.currentView.value !== 'code') { + this.currentView.set('code'); + } + const doc = this.#editorStore.documents.get()[fullPath]; + if (!doc) { + await artifact.runner.runAction(data, isStreaming); + } - artifact.runner.runAction(data); + this.#editorStore.updateFile(fullPath, data.action.content); + + if (!isStreaming) { + this.resetCurrentDocument(); + await artifact.runner.runAction(data); + } + } else { + artifact.runner.runAction(data); + } } #getArtifact(id: string) { @@ -336,24 +367,25 @@ export class WorkbenchStore { } async pushToGitHub(repoName: string, githubUsername: string, ghToken: string) { - + try { // Get the GitHub auth token from environment variables const githubToken = ghToken; - + const owner = githubUsername; - + if (!githubToken) { throw new Error('GitHub token is not set in environment variables'); } - + // Initialize Octokit with the auth token const octokit = new Octokit({ auth: githubToken }); - + // Check if the repository already exists before creating it - let repo + let repo: RestEndpointMethodTypes["repos"]["get"]["response"]['data'] try { - repo = await octokit.repos.get({ owner: owner, repo: repoName }); + let resp = await octokit.repos.get({ owner: owner, repo: repoName }); + repo = resp.data } catch (error) { if (error instanceof Error && 'status' in error && error.status === 404) { // Repository doesn't exist, so create a new one @@ -368,13 +400,13 @@ export class WorkbenchStore { throw error; // Some other error occurred } } - + // Get all files const files = this.files.get(); if (!files || Object.keys(files).length === 0) { throw new Error('No files found to push'); } - + // Create blobs for each file const blobs = await Promise.all( Object.entries(files).map(async ([filePath, dirent]) => { @@ -389,13 +421,13 @@ export class WorkbenchStore { } }) ); - + const validBlobs = blobs.filter(Boolean); // Filter out any undefined blobs - + if (validBlobs.length === 0) { throw new Error('No valid files to push'); } - + // Get the latest commit SHA (assuming main branch, update dynamically if needed) const { data: ref } = await octokit.git.getRef({ owner: repo.owner.login, @@ -403,7 +435,7 @@ export class WorkbenchStore { ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch }); const latestCommitSha = ref.object.sha; - + // Create a new tree const { data: newTree } = await octokit.git.createTree({ owner: repo.owner.login, @@ -416,7 +448,7 @@ export class WorkbenchStore { sha: blob!.sha, })), }); - + // Create a new commit const { data: newCommit } = await octokit.git.createCommit({ owner: repo.owner.login, @@ -425,7 +457,7 @@ export class WorkbenchStore { tree: newTree.sha, parents: [latestCommitSha], }); - + // Update the reference await octokit.git.updateRef({ owner: repo.owner.login, @@ -433,7 +465,7 @@ export class WorkbenchStore { ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch sha: newCommit.sha, }); - + alert(`Repository created and code pushed: ${repo.html_url}`); } catch (error) { console.error('Error pushing to GitHub:', error instanceof Error ? error.message : String(error)); diff --git a/app/routes/api.enhancer.ts b/app/routes/api.enhancer.ts index 895bccafc..a4cce9e48 100644 --- a/app/routes/api.enhancer.ts +++ b/app/routes/api.enhancer.ts @@ -12,7 +12,11 @@ export async function action(args: ActionFunctionArgs) { } async function enhancerAction({ context, request }: ActionFunctionArgs) { - const { model, message } = await request.json<{ message: string; model: ModelInfo }>(); + const { model, message, apiKeys } = await request.json<{ + message: string; + model: ModelInfo; + apiKeys?: Record; + }>(); try { const result = await streamText( @@ -32,28 +36,42 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) { }, ], context.cloudflare.env, + undefined, + apiKeys, ); const transformStream = new TransformStream({ transform(chunk, controller) { - const processedChunk = decoder - .decode(chunk) - .split('\n') - .filter((line) => line !== '') - .map(parseStreamPart) - .map((part) => part.value) - .join(''); - - controller.enqueue(encoder.encode(processedChunk)); + const text = decoder.decode(chunk); + const lines = text.split('\n').filter((line) => line.trim() !== ''); + + for (const line of lines) { + try { + const parsed = parseStreamPart(line); + if (parsed.type === 'text') { + controller.enqueue(encoder.encode(parsed.value)); + } + } catch (e) { + // Skip invalid JSON lines + console.warn('Failed to parse stream part:', line); + } + } }, }); const transformedStream = result.toAIStream().pipeThrough(transformStream); return new StreamingTextResponse(transformedStream); - } catch (error) { + } catch (error: unknown) { console.log(error); + if (error instanceof Error && error.message?.includes('API key')) { + throw new Response('Invalid or missing API key', { + status: 401, + statusText: 'Unauthorized', + }); + } + throw new Response(null, { status: 500, statusText: 'Internal Server Error', diff --git a/app/types/actions.ts b/app/types/actions.ts index b81127aa1..08c1f39a2 100644 --- a/app/types/actions.ts +++ b/app/types/actions.ts @@ -13,6 +13,10 @@ export interface ShellAction extends BaseAction { type: 'shell'; } -export type BoltAction = FileAction | ShellAction; +export interface StartAction extends BaseAction { + type: 'start'; +} + +export type BoltAction = FileAction | ShellAction | StartAction; export type BoltActionData = BoltAction | BaseAction; diff --git a/app/types/terminal.ts b/app/types/terminal.ts index 75ae3a3ad..48e50b471 100644 --- a/app/types/terminal.ts +++ b/app/types/terminal.ts @@ -5,4 +5,5 @@ export interface ITerminal { reset: () => void; write: (data: string) => void; onData: (cb: (data: string) => void) => void; + input: (data: string) => void; } diff --git a/app/utils/constants.ts b/app/utils/constants.ts index 2e2b24330..328071f7f 100644 --- a/app/utils/constants.ts +++ b/app/utils/constants.ts @@ -3,9 +3,8 @@ import type { ModelInfo, OllamaApiResponse, OllamaModel } from './types'; export const WORK_DIR_NAME = 'project'; export const WORK_DIR = `/home/${WORK_DIR_NAME}`; export const MODIFICATIONS_TAG_NAME = 'bolt_file_modifications'; -export const MODEL_REGEX = /^\[Model: (.*?)\]\n\n/; -export const DEFAULT_MODEL = 'google/gemini-flash-1.5-exp'; -export const DEFAULT_PROVIDER = 'OpenRouter'; +export const DEFAULT_MODEL = 'claude-3-5-sonnet-latest'; +export const DEFAULT_PROVIDER = 'Anthropic'; const staticProviders: string[] = [ 'Ollama', @@ -16,6 +15,8 @@ const staticProviders: string[] = [ 'Groq', 'Deepseek', 'OpenAILike', + 'Google', + 'xAI', ]; // const staticModels: ModelInfo[] = [ diff --git a/app/utils/shell.ts b/app/utils/shell.ts index 1c5c834d8..d45e8a6ba 100644 --- a/app/utils/shell.ts +++ b/app/utils/shell.ts @@ -1,6 +1,7 @@ -import type { WebContainer } from '@webcontainer/api'; +import type { WebContainer, WebContainerProcess } from '@webcontainer/api'; import type { ITerminal } from '~/types/terminal'; import { withResolvers } from './promises'; +import { atom } from 'nanostores'; export async function newShellProcess(webcontainer: WebContainer, terminal: ITerminal) { const args: string[] = []; @@ -19,7 +20,6 @@ export async function newShellProcess(webcontainer: WebContainer, terminal: ITer const jshReady = withResolvers(); let isInteractive = false; - output.pipeTo( new WritableStream({ write(data) { @@ -40,6 +40,8 @@ export async function newShellProcess(webcontainer: WebContainer, terminal: ITer ); terminal.onData((data) => { + // console.log('terminal onData', { data, isInteractive }); + if (isInteractive) { input.write(data); } @@ -49,3 +51,145 @@ export async function newShellProcess(webcontainer: WebContainer, terminal: ITer return process; } + + + +export class BoltShell { + #initialized: (() => void) | undefined + #readyPromise: Promise + #webcontainer: WebContainer | undefined + #terminal: ITerminal | undefined + #process: WebContainerProcess | undefined + executionState = atom<{ sessionId: string, active: boolean, executionPrms?: Promise } | undefined>() + #outputStream: ReadableStreamDefaultReader | undefined + #shellInputStream: WritableStreamDefaultWriter | undefined + constructor() { + this.#readyPromise = new Promise((resolve) => { + this.#initialized = resolve + }) + } + ready() { + return this.#readyPromise; + } + async init(webcontainer: WebContainer, terminal: ITerminal) { + this.#webcontainer = webcontainer + this.#terminal = terminal + let callback = (data: string) => { + console.log(data) + } + let { process, output } = await this.newBoltShellProcess(webcontainer, terminal) + this.#process = process + this.#outputStream = output.getReader() + await this.waitTillOscCode('interactive') + this.#initialized?.() + } + get terminal() { + return this.#terminal + } + get process() { + return this.#process + } + async executeCommand(sessionId: string, command: string) { + if (!this.process || !this.terminal) { + return + } + let state = this.executionState.get() + + //interrupt the current execution + // this.#shellInputStream?.write('\x03'); + this.terminal.input('\x03'); + if (state && state.executionPrms) { + await state.executionPrms + } + //start a new execution + this.terminal.input(command.trim() + '\n'); + + //wait for the execution to finish + let executionPrms = this.getCurrentExecutionResult() + this.executionState.set({ sessionId, active: true, executionPrms }) + + let resp = await executionPrms + this.executionState.set({ sessionId, active: false }) + return resp + + } + async newBoltShellProcess(webcontainer: WebContainer, terminal: ITerminal) { + const args: string[] = []; + + // we spawn a JSH process with a fallback cols and rows in case the process is not attached yet to a visible terminal + const process = await webcontainer.spawn('/bin/jsh', ['--osc', ...args], { + terminal: { + cols: terminal.cols ?? 80, + rows: terminal.rows ?? 15, + }, + }); + + const input = process.input.getWriter(); + this.#shellInputStream = input; + const [internalOutput, terminalOutput] = process.output.tee(); + + const jshReady = withResolvers(); + + let isInteractive = false; + terminalOutput.pipeTo( + new WritableStream({ + write(data) { + if (!isInteractive) { + const [, osc] = data.match(/\x1b\]654;([^\x07]+)\x07/) || []; + + if (osc === 'interactive') { + // wait until we see the interactive OSC + isInteractive = true; + + jshReady.resolve(); + } + } + + terminal.write(data); + }, + }), + ); + + terminal.onData((data) => { + // console.log('terminal onData', { data, isInteractive }); + + if (isInteractive) { + input.write(data); + } + }); + + await jshReady.promise; + + return { process, output: internalOutput }; + } + async getCurrentExecutionResult() { + let { output, exitCode } = await this.waitTillOscCode('exit') + return { output, exitCode }; + } + async waitTillOscCode(waitCode: string) { + let fullOutput = ''; + let exitCode: number = 0; + if (!this.#outputStream) return { output: fullOutput, exitCode }; + let tappedStream = this.#outputStream + + while (true) { + const { value, done } = await tappedStream.read(); + if (done) break; + const text = value || ''; + fullOutput += text; + + // Check if command completion signal with exit code + const [, osc, , pid, code] = text.match(/\x1b\]654;([^\x07=]+)=?((-?\d+):(\d+))?\x07/) || []; + if (osc === 'exit') { + exitCode = parseInt(code, 10); + } + if (osc === waitCode) { + break; + } + } + return { output: fullOutput, exitCode }; + } +} +export function newBoltShellProcess() { + return new BoltShell(); +} diff --git a/package.json b/package.json index 3cba1cf56..ce8e95d0d 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "start": "bindings=$(./bindings.sh) && wrangler pages dev ./build/client $bindings", "dockerstart": "bindings=$(./bindings.sh) && wrangler pages dev ./build/client $bindings --ip 0.0.0.0 --port 5173 --no-show-interactive-dev-session", "dockerrun": "docker run -it -d --name bolt-ai-live -p 5173:5173 --env-file .env.local bolt-ai", - "dockerbuild:prod": "docker build -t bolt-ai:production bolt-ai:latest --target bolt-ai-production .", + "dockerbuild:prod": "docker build -t bolt-ai:production -t bolt-ai:latest --target bolt-ai-production .", "dockerbuild": "docker build -t bolt-ai:development -t bolt-ai:latest --target bolt-ai-development .", "typecheck": "tsc", "typegen": "wrangler types", @@ -117,5 +117,5 @@ "resolutions": { "@typescript-eslint/utils": "^8.0.0-alpha.30" }, - "packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228" + "packageManager": "pnpm@9.4.0" } diff --git a/vite.config.ts b/vite.config.ts index 625390702..9c94ceae3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -27,7 +27,7 @@ export default defineConfig((config) => { chrome129IssuePlugin(), config.mode === 'production' && optimizeCssModules({ apply: 'build' }), ], - envPrefix:["VITE_","OPENAI_LIKE_API_","OLLAMA_API_BASE_URL"], + envPrefix:["VITE_","OPENAI_LIKE_API_","OLLAMA_API_BASE_URL","LMSTUDIO_API_BASE_URL"], css: { preprocessorOptions: { scss: { diff --git a/worker-configuration.d.ts b/worker-configuration.d.ts index 215987e78..9f64de3de 100644 --- a/worker-configuration.d.ts +++ b/worker-configuration.d.ts @@ -8,4 +8,5 @@ interface Env { OPENAI_LIKE_API_KEY: string; OPENAI_LIKE_API_BASE_URL: string; DEEPSEEK_API_KEY: string; + LMSTUDIO_API_BASE_URL: string; }