Eduards commited on
Commit
6b13ecd
·
2 Parent(s): 756d3f2 a081f8b

Merge remote-tracking branch 'coleam00/main' into addGetKeyLinks

Browse files
.github/workflows/semantic-pr.yaml DELETED
@@ -1,32 +0,0 @@
1
- name: Semantic Pull Request
2
- on:
3
- pull_request_target:
4
- types: [opened, reopened, edited, synchronize]
5
- permissions:
6
- pull-requests: read
7
- jobs:
8
- main:
9
- name: Validate PR Title
10
- runs-on: ubuntu-latest
11
- steps:
12
- # https://github.com/amannn/action-semantic-pull-request/releases/tag/v5.5.3
13
- - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017
14
- env:
15
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
16
- with:
17
- subjectPattern: ^(?![A-Z]).+$
18
- subjectPatternError: |
19
- The subject "{subject}" found in the pull request title "{title}"
20
- didn't match the configured pattern. Please ensure that the subject
21
- doesn't start with an uppercase character.
22
- types: |
23
- fix
24
- feat
25
- chore
26
- build
27
- ci
28
- perf
29
- docs
30
- refactor
31
- revert
32
- test
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/components/chat/Artifact.tsx CHANGED
@@ -151,7 +151,13 @@ const ActionList = memo(({ actions }: ActionListProps) => {
151
  <div className="flex items-center gap-1.5 text-sm">
152
  <div className={classNames('text-lg', getIconColor(action.status))}>
153
  {status === 'running' ? (
154
- <div className="i-svg-spinners:90-ring-with-bg"></div>
 
 
 
 
 
 
155
  ) : status === 'pending' ? (
156
  <div className="i-ph:circle-duotone"></div>
157
  ) : status === 'complete' ? (
@@ -171,9 +177,19 @@ const ActionList = memo(({ actions }: ActionListProps) => {
171
  <div className="flex items-center w-full min-h-[28px]">
172
  <span className="flex-1">Run command</span>
173
  </div>
 
 
 
 
 
 
 
 
 
 
174
  ) : null}
175
  </div>
176
- {type === 'shell' && (
177
  <ShellCodeBlock
178
  classsName={classNames('mt-1', {
179
  'mb-3.5': !isLast,
 
151
  <div className="flex items-center gap-1.5 text-sm">
152
  <div className={classNames('text-lg', getIconColor(action.status))}>
153
  {status === 'running' ? (
154
+ <>
155
+ {type !== 'start' ? (
156
+ <div className="i-svg-spinners:90-ring-with-bg"></div>
157
+ ) : (
158
+ <div className="i-ph:terminal-window-duotone"></div>
159
+ )}
160
+ </>
161
  ) : status === 'pending' ? (
162
  <div className="i-ph:circle-duotone"></div>
163
  ) : status === 'complete' ? (
 
177
  <div className="flex items-center w-full min-h-[28px]">
178
  <span className="flex-1">Run command</span>
179
  </div>
180
+ ) : type === 'start' ? (
181
+ <a
182
+ onClick={(e) => {
183
+ e.preventDefault();
184
+ workbenchStore.currentView.set('preview');
185
+ }}
186
+ className="flex items-center w-full min-h-[28px]"
187
+ >
188
+ <span className="flex-1">Start Application</span>
189
+ </a>
190
  ) : null}
191
  </div>
192
+ {(type === 'shell' || type === 'start') && (
193
  <ShellCodeBlock
194
  classsName={classNames('mt-1', {
195
  'mb-3.5': !isLast,
app/components/chat/BaseChat.tsx CHANGED
@@ -33,7 +33,7 @@ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, prov
33
  value={provider?.name}
34
  onChange={(e) => {
35
  setProvider(providerList.find(p => p.name === e.target.value));
36
- const firstModel = [...modelList].find(m => m.provider == e.target.value);
37
  setModel(firstModel ? firstModel.name : '');
38
  }}
39
  className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
@@ -49,11 +49,13 @@ const ModelSelector = ({ model, setModel, provider, setProvider, modelList, prov
49
  onChange={(e) => setModel(e.target.value)}
50
  className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
51
  >
52
- {[...modelList].filter(e => e.provider == provider?.name && e.name).map((modelOption) => (
53
- <option key={modelOption.name} value={modelOption.name}>
54
- {modelOption.label}
55
- </option>
56
- ))}
 
 
57
  </select>
58
  </div>
59
  );
@@ -72,10 +74,10 @@ interface BaseChatProps {
72
  enhancingPrompt?: boolean;
73
  promptEnhanced?: boolean;
74
  input?: string;
75
- model: string;
76
- setModel: (model: string) => void;
77
- provider: ProviderInfo;
78
- setProvider: (provider: ProviderInfo) => void;
79
  handleStop?: () => void;
80
  sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
81
  handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
@@ -136,7 +138,7 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
136
  expires: 30, // 30 days
137
  secure: true, // Only send over HTTPS
138
  sameSite: 'strict', // Protect against CSRF
139
- path: '/' // Accessible across the site
140
  });
141
  } catch (error) {
142
  console.error('Error saving API keys to cookies:', error);
@@ -274,7 +276,9 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
274
  </div>
275
  {input.length > 3 ? (
276
  <div className="text-xs text-bolt-elements-textTertiary">
277
- Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> + <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> for a new line
 
 
278
  </div>
279
  ) : null}
280
  </div>
 
33
  value={provider?.name}
34
  onChange={(e) => {
35
  setProvider(providerList.find(p => p.name === e.target.value));
36
+ const firstModel = [...modelList].find((m) => m.provider == e.target.value);
37
  setModel(firstModel ? firstModel.name : '');
38
  }}
39
  className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
 
49
  onChange={(e) => setModel(e.target.value)}
50
  className="flex-1 p-2 rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-prompt-background text-bolt-elements-textPrimary focus:outline-none focus:ring-2 focus:ring-bolt-elements-focus transition-all"
51
  >
52
+ {[...modelList]
53
+ .filter((e) => e.provider == provider?.name && e.name)
54
+ .map((modelOption) => (
55
+ <option key={modelOption.name} value={modelOption.name}>
56
+ {modelOption.label}
57
+ </option>
58
+ ))}
59
  </select>
60
  </div>
61
  );
 
74
  enhancingPrompt?: boolean;
75
  promptEnhanced?: boolean;
76
  input?: string;
77
+ model?: string;
78
+ setModel?: (model: string) => void;
79
+ provider?: ProviderInfo;
80
+ setProvider?: (provider: ProviderInfo) => void;
81
  handleStop?: () => void;
82
  sendMessage?: (event: React.UIEvent, messageInput?: string) => void;
83
  handleInputChange?: (event: React.ChangeEvent<HTMLTextAreaElement>) => void;
 
138
  expires: 30, // 30 days
139
  secure: true, // Only send over HTTPS
140
  sameSite: 'strict', // Protect against CSRF
141
+ path: '/', // Accessible across the site
142
  });
143
  } catch (error) {
144
  console.error('Error saving API keys to cookies:', error);
 
276
  </div>
277
  {input.length > 3 ? (
278
  <div className="text-xs text-bolt-elements-textTertiary">
279
+ Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> +{' '}
280
+ <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> for
281
+ a new line
282
  </div>
283
  ) : null}
284
  </div>
app/components/chat/Chat.client.tsx CHANGED
@@ -74,8 +74,14 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
74
  const textareaRef = useRef<HTMLTextAreaElement>(null);
75
 
76
  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
77
- const [model, setModel] = useState(DEFAULT_MODEL);
78
- const [provider, setProvider] = useState(DEFAULT_PROVIDER);
 
 
 
 
 
 
79
 
80
  const { showChat } = useStore(chatStore);
81
 
@@ -216,6 +222,16 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
216
  }
217
  }, []);
218
 
 
 
 
 
 
 
 
 
 
 
219
  return (
220
  <BaseChat
221
  ref={animationScope}
@@ -228,9 +244,9 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
228
  promptEnhanced={promptEnhanced}
229
  sendMessage={sendMessage}
230
  model={model}
231
- setModel={setModel}
232
  provider={provider}
233
- setProvider={setProvider}
234
  messageRef={messageRef}
235
  scrollRef={scrollRef}
236
  handleInputChange={handleInputChange}
@@ -246,10 +262,16 @@ export const ChatImpl = memo(({ initialMessages, storeMessageHistory }: ChatProp
246
  };
247
  })}
248
  enhancePrompt={() => {
249
- enhancePrompt(input, (input) => {
250
- setInput(input);
251
- scrollTextArea();
252
- });
 
 
 
 
 
 
253
  }}
254
  />
255
  );
 
74
  const textareaRef = useRef<HTMLTextAreaElement>(null);
75
 
76
  const [chatStarted, setChatStarted] = useState(initialMessages.length > 0);
77
+ const [model, setModel] = useState(() => {
78
+ const savedModel = Cookies.get('selectedModel');
79
+ return savedModel || DEFAULT_MODEL;
80
+ });
81
+ const [provider, setProvider] = useState(() => {
82
+ const savedProvider = Cookies.get('selectedProvider');
83
+ return savedProvider || DEFAULT_PROVIDER;
84
+ });
85
 
86
  const { showChat } = useStore(chatStore);
87
 
 
222
  }
223
  }, []);
224
 
225
+ const handleModelChange = (newModel: string) => {
226
+ setModel(newModel);
227
+ Cookies.set('selectedModel', newModel, { expires: 30 });
228
+ };
229
+
230
+ const handleProviderChange = (newProvider: string) => {
231
+ setProvider(newProvider);
232
+ Cookies.set('selectedProvider', newProvider, { expires: 30 });
233
+ };
234
+
235
  return (
236
  <BaseChat
237
  ref={animationScope}
 
244
  promptEnhanced={promptEnhanced}
245
  sendMessage={sendMessage}
246
  model={model}
247
+ setModel={handleModelChange}
248
  provider={provider}
249
+ setProvider={handleProviderChange}
250
  messageRef={messageRef}
251
  scrollRef={scrollRef}
252
  handleInputChange={handleInputChange}
 
262
  };
263
  })}
264
  enhancePrompt={() => {
265
+ enhancePrompt(
266
+ input,
267
+ (input) => {
268
+ setInput(input);
269
+ scrollTextArea();
270
+ },
271
+ model,
272
+ provider,
273
+ apiKeys
274
+ );
275
  }}
276
  />
277
  );
app/components/workbench/EditorPanel.tsx CHANGED
@@ -18,7 +18,7 @@ import { themeStore } from '~/lib/stores/theme';
18
  import { workbenchStore } from '~/lib/stores/workbench';
19
  import { classNames } from '~/utils/classNames';
20
  import { WORK_DIR } from '~/utils/constants';
21
- import { renderLogger } from '~/utils/logger';
22
  import { isMobile } from '~/utils/mobile';
23
  import { FileBreadcrumb } from './FileBreadcrumb';
24
  import { FileTree } from './FileTree';
@@ -199,25 +199,48 @@ export const EditorPanel = memo(
199
  <div className="h-full">
200
  <div className="bg-bolt-elements-terminals-background h-full flex flex-col">
201
  <div className="flex items-center bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor gap-1.5 min-h-[34px] p-2">
202
- {Array.from({ length: terminalCount }, (_, index) => {
203
  const isActive = activeTerminal === index;
204
 
205
  return (
206
- <button
207
- key={index}
208
- className={classNames(
209
- 'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
210
- {
211
- 'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textPrimary': isActive,
212
- 'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
213
- !isActive,
214
- },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  )}
216
- onClick={() => setActiveTerminal(index)}
217
- >
218
- <div className="i-ph:terminal-window-duotone text-lg" />
219
- Terminal {terminalCount > 1 && index + 1}
220
- </button>
221
  );
222
  })}
223
  {terminalCount < MAX_TERMINALS && <IconButton icon="i-ph:plus" size="md" onClick={addTerminal} />}
@@ -229,9 +252,26 @@ export const EditorPanel = memo(
229
  onClick={() => workbenchStore.toggleTerminal(false)}
230
  />
231
  </div>
232
- {Array.from({ length: terminalCount }, (_, index) => {
233
  const isActive = activeTerminal === index;
 
 
234
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
235
  return (
236
  <Terminal
237
  key={index}
 
18
  import { workbenchStore } from '~/lib/stores/workbench';
19
  import { classNames } from '~/utils/classNames';
20
  import { WORK_DIR } from '~/utils/constants';
21
+ import { logger, renderLogger } from '~/utils/logger';
22
  import { isMobile } from '~/utils/mobile';
23
  import { FileBreadcrumb } from './FileBreadcrumb';
24
  import { FileTree } from './FileTree';
 
199
  <div className="h-full">
200
  <div className="bg-bolt-elements-terminals-background h-full flex flex-col">
201
  <div className="flex items-center bg-bolt-elements-background-depth-2 border-y border-bolt-elements-borderColor gap-1.5 min-h-[34px] p-2">
202
+ {Array.from({ length: terminalCount + 1 }, (_, index) => {
203
  const isActive = activeTerminal === index;
204
 
205
  return (
206
+ <>
207
+ {index == 0 ? (
208
+ <button
209
+ key={index}
210
+ className={classNames(
211
+ 'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
212
+ {
213
+ 'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textSecondary hover:text-bolt-elements-textPrimary':
214
+ isActive,
215
+ 'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
216
+ !isActive,
217
+ },
218
+ )}
219
+ onClick={() => setActiveTerminal(index)}
220
+ >
221
+ <div className="i-ph:terminal-window-duotone text-lg" />
222
+ Bolt Terminal
223
+ </button>
224
+ ) : (
225
+ <>
226
+ <button
227
+ key={index}
228
+ className={classNames(
229
+ 'flex items-center text-sm cursor-pointer gap-1.5 px-3 py-2 h-full whitespace-nowrap rounded-full',
230
+ {
231
+ 'bg-bolt-elements-terminals-buttonBackground text-bolt-elements-textPrimary': isActive,
232
+ 'bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary hover:bg-bolt-elements-terminals-buttonBackground':
233
+ !isActive,
234
+ },
235
+ )}
236
+ onClick={() => setActiveTerminal(index)}
237
+ >
238
+ <div className="i-ph:terminal-window-duotone text-lg" />
239
+ Terminal {terminalCount > 1 && index}
240
+ </button>
241
+ </>
242
  )}
243
+ </>
 
 
 
 
244
  );
245
  })}
246
  {terminalCount < MAX_TERMINALS && <IconButton icon="i-ph:plus" size="md" onClick={addTerminal} />}
 
252
  onClick={() => workbenchStore.toggleTerminal(false)}
253
  />
254
  </div>
255
+ {Array.from({ length: terminalCount + 1 }, (_, index) => {
256
  const isActive = activeTerminal === index;
257
+ if (index == 0) {
258
+ logger.info('Starting bolt terminal');
259
 
260
+ return (
261
+ <Terminal
262
+ key={index}
263
+ className={classNames('h-full overflow-hidden', {
264
+ hidden: !isActive,
265
+ })}
266
+ ref={(ref) => {
267
+ terminalRefs.current.push(ref);
268
+ }}
269
+ onTerminalReady={(terminal) => workbenchStore.attachBoltTerminal(terminal)}
270
+ onTerminalResize={(cols, rows) => workbenchStore.onTerminalResize(cols, rows)}
271
+ theme={theme}
272
+ />
273
+ );
274
+ }
275
  return (
276
  <Terminal
277
  key={index}
app/lib/.server/llm/prompts.ts CHANGED
@@ -174,10 +174,16 @@ You are Bolt, an expert AI assistant and exceptional senior software developer w
174
 
175
  - When Using \`npx\`, ALWAYS provide the \`--yes\` flag.
176
  - When running multiple shell commands, use \`&&\` to run them sequentially.
177
- - 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.
178
 
179
  - file: For writing new files or updating existing files. For each file add a \`filePath\` attribute to the opening \`<boltAction>\` 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.
180
 
 
 
 
 
 
 
181
  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.
182
 
183
  10. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \`package.json\` then you should create that first!
@@ -265,7 +271,7 @@ Here are some examples of correct usage of artifacts:
265
  ...
266
  </boltAction>
267
 
268
- <boltAction type="shell">
269
  npm run dev
270
  </boltAction>
271
  </boltArtifact>
@@ -322,7 +328,7 @@ Here are some examples of correct usage of artifacts:
322
  ...
323
  </boltAction>
324
 
325
- <boltAction type="shell">
326
  npm run dev
327
  </boltAction>
328
  </boltArtifact>
 
174
 
175
  - When Using \`npx\`, ALWAYS provide the \`--yes\` flag.
176
  - When running multiple shell commands, use \`&&\` to run them sequentially.
177
+ - ULTRA IMPORTANT: Do NOT re-run a dev command with shell action use dev action to run dev commands
178
 
179
  - file: For writing new files or updating existing files. For each file add a \`filePath\` attribute to the opening \`<boltAction>\` 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.
180
 
181
+ - start: For starting development server.
182
+ - Use to start application if not already started or NEW dependencies added
183
+ - Only use this action when you need to run a dev server or start the application
184
+ - ULTRA IMORTANT: do NOT re-run a dev server if files updated, existing dev server can autometically detect changes and executes the file changes
185
+
186
+
187
  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.
188
 
189
  10. ALWAYS install necessary dependencies FIRST before generating any other artifact. If that requires a \`package.json\` then you should create that first!
 
271
  ...
272
  </boltAction>
273
 
274
+ <boltAction type="start">
275
  npm run dev
276
  </boltAction>
277
  </boltArtifact>
 
328
  ...
329
  </boltAction>
330
 
331
+ <boltAction type="start">
332
  npm run dev
333
  </boltAction>
334
  </boltArtifact>
app/lib/hooks/useMessageParser.ts CHANGED
@@ -36,6 +36,10 @@ const messageParser = new StreamingMessageParser({
36
 
37
  workbenchStore.runAction(data);
38
  },
 
 
 
 
39
  },
40
  });
41
 
 
36
 
37
  workbenchStore.runAction(data);
38
  },
39
+ onActionStream: (data) => {
40
+ logger.trace('onActionStream', data.action);
41
+ workbenchStore.runAction(data, true);
42
+ },
43
  },
44
  });
45
 
app/lib/hooks/usePromptEnhancer.ts CHANGED
@@ -12,41 +12,55 @@ export function usePromptEnhancer() {
12
  setPromptEnhanced(false);
13
  };
14
 
15
- const enhancePrompt = async (input: string, setInput: (value: string) => void) => {
 
 
 
 
 
 
16
  setEnhancingPrompt(true);
17
  setPromptEnhanced(false);
18
-
 
 
 
 
 
 
 
 
 
 
19
  const response = await fetch('/api/enhancer', {
20
  method: 'POST',
21
- body: JSON.stringify({
22
- message: input,
23
- }),
24
  });
25
-
26
  const reader = response.body?.getReader();
27
-
28
  const originalInput = input;
29
-
30
  if (reader) {
31
  const decoder = new TextDecoder();
32
-
33
  let _input = '';
34
  let _error;
35
-
36
  try {
37
  setInput('');
38
-
39
  while (true) {
40
  const { value, done } = await reader.read();
41
-
42
  if (done) {
43
  break;
44
  }
45
-
46
  _input += decoder.decode(value);
47
-
48
  logger.trace('Set input', _input);
49
-
50
  setInput(_input);
51
  }
52
  } catch (error) {
@@ -56,10 +70,10 @@ export function usePromptEnhancer() {
56
  if (_error) {
57
  logger.error(_error);
58
  }
59
-
60
  setEnhancingPrompt(false);
61
  setPromptEnhanced(true);
62
-
63
  setTimeout(() => {
64
  setInput(_input);
65
  });
 
12
  setPromptEnhanced(false);
13
  };
14
 
15
+ const enhancePrompt = async (
16
+ input: string,
17
+ setInput: (value: string) => void,
18
+ model: string,
19
+ provider: string,
20
+ apiKeys?: Record<string, string>
21
+ ) => {
22
  setEnhancingPrompt(true);
23
  setPromptEnhanced(false);
24
+
25
+ const requestBody: any = {
26
+ message: input,
27
+ model,
28
+ provider,
29
+ };
30
+
31
+ if (apiKeys) {
32
+ requestBody.apiKeys = apiKeys;
33
+ }
34
+
35
  const response = await fetch('/api/enhancer', {
36
  method: 'POST',
37
+ body: JSON.stringify(requestBody),
 
 
38
  });
39
+
40
  const reader = response.body?.getReader();
41
+
42
  const originalInput = input;
43
+
44
  if (reader) {
45
  const decoder = new TextDecoder();
46
+
47
  let _input = '';
48
  let _error;
49
+
50
  try {
51
  setInput('');
52
+
53
  while (true) {
54
  const { value, done } = await reader.read();
55
+
56
  if (done) {
57
  break;
58
  }
59
+
60
  _input += decoder.decode(value);
61
+
62
  logger.trace('Set input', _input);
63
+
64
  setInput(_input);
65
  }
66
  } catch (error) {
 
70
  if (_error) {
71
  logger.error(_error);
72
  }
73
+
74
  setEnhancingPrompt(false);
75
  setPromptEnhanced(true);
76
+
77
  setTimeout(() => {
78
  setInput(_input);
79
  });
app/lib/runtime/action-runner.ts CHANGED
@@ -1,10 +1,12 @@
1
- import { WebContainer } from '@webcontainer/api';
2
- import { map, type MapStore } from 'nanostores';
3
  import * as nodePath from 'node:path';
4
  import type { BoltAction } from '~/types/actions';
5
  import { createScopedLogger } from '~/utils/logger';
6
  import { unreachable } from '~/utils/unreachable';
7
  import type { ActionCallbackData } from './message-parser';
 
 
8
 
9
  const logger = createScopedLogger('ActionRunner');
10
 
@@ -36,11 +38,14 @@ type ActionsMap = MapStore<Record<string, ActionState>>;
36
  export class ActionRunner {
37
  #webcontainer: Promise<WebContainer>;
38
  #currentExecutionPromise: Promise<void> = Promise.resolve();
39
-
 
40
  actions: ActionsMap = map({});
41
 
42
- constructor(webcontainerPromise: Promise<WebContainer>) {
43
  this.#webcontainer = webcontainerPromise;
 
 
44
  }
45
 
46
  addAction(data: ActionCallbackData) {
@@ -72,7 +77,7 @@ export class ActionRunner {
72
  });
73
  }
74
 
75
- async runAction(data: ActionCallbackData) {
76
  const { actionId } = data;
77
  const action = this.actions.get()[actionId];
78
 
@@ -83,19 +88,22 @@ export class ActionRunner {
83
  if (action.executed) {
84
  return;
85
  }
 
 
 
86
 
87
- this.#updateAction(actionId, { ...action, ...data.action, executed: true });
88
 
89
  this.#currentExecutionPromise = this.#currentExecutionPromise
90
  .then(() => {
91
- return this.#executeAction(actionId);
92
  })
93
  .catch((error) => {
94
  console.error('Action failed:', error);
95
  });
96
  }
97
 
98
- async #executeAction(actionId: string) {
99
  const action = this.actions.get()[actionId];
100
 
101
  this.#updateAction(actionId, { status: 'running' });
@@ -110,11 +118,16 @@ export class ActionRunner {
110
  await this.#runFileAction(action);
111
  break;
112
  }
 
 
 
 
113
  }
114
 
115
- this.#updateAction(actionId, { status: action.abortSignal.aborted ? 'aborted' : 'complete' });
116
  } catch (error) {
117
  this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
 
118
 
119
  // re-throw the error to be caught in the promise chain
120
  throw error;
@@ -125,28 +138,38 @@ export class ActionRunner {
125
  if (action.type !== 'shell') {
126
  unreachable('Expected shell action');
127
  }
 
 
 
 
 
 
 
 
 
128
 
129
- const webcontainer = await this.#webcontainer;
130
-
131
- const process = await webcontainer.spawn('jsh', ['-c', action.content], {
132
- env: { npm_config_yes: true },
133
- });
134
-
135
- action.abortSignal.addEventListener('abort', () => {
136
- process.kill();
137
- });
138
-
139
- process.output.pipeTo(
140
- new WritableStream({
141
- write(data) {
142
- console.log(data);
143
- },
144
- }),
145
- );
146
 
147
- const exitCode = await process.exit;
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
- logger.debug(`Process terminated with code ${exitCode}`);
 
 
 
150
  }
151
 
152
  async #runFileAction(action: ActionState) {
@@ -177,7 +200,6 @@ export class ActionRunner {
177
  logger.error('Failed to write file\n\n', error);
178
  }
179
  }
180
-
181
  #updateAction(id: string, newState: ActionStateUpdate) {
182
  const actions = this.actions.get();
183
 
 
1
+ import { WebContainer, type WebContainerProcess } from '@webcontainer/api';
2
+ import { atom, map, type MapStore } from 'nanostores';
3
  import * as nodePath from 'node:path';
4
  import type { BoltAction } from '~/types/actions';
5
  import { createScopedLogger } from '~/utils/logger';
6
  import { unreachable } from '~/utils/unreachable';
7
  import type { ActionCallbackData } from './message-parser';
8
+ import type { ITerminal } from '~/types/terminal';
9
+ import type { BoltShell } from '~/utils/shell';
10
 
11
  const logger = createScopedLogger('ActionRunner');
12
 
 
38
  export class ActionRunner {
39
  #webcontainer: Promise<WebContainer>;
40
  #currentExecutionPromise: Promise<void> = Promise.resolve();
41
+ #shellTerminal: () => BoltShell;
42
+ runnerId = atom<string>(`${Date.now()}`);
43
  actions: ActionsMap = map({});
44
 
45
+ constructor(webcontainerPromise: Promise<WebContainer>, getShellTerminal: () => BoltShell) {
46
  this.#webcontainer = webcontainerPromise;
47
+ this.#shellTerminal = getShellTerminal;
48
+
49
  }
50
 
51
  addAction(data: ActionCallbackData) {
 
77
  });
78
  }
79
 
80
+ async runAction(data: ActionCallbackData, isStreaming: boolean = false) {
81
  const { actionId } = data;
82
  const action = this.actions.get()[actionId];
83
 
 
88
  if (action.executed) {
89
  return;
90
  }
91
+ if (isStreaming && action.type !== 'file') {
92
+ return;
93
+ }
94
 
95
+ this.#updateAction(actionId, { ...action, ...data.action, executed: !isStreaming });
96
 
97
  this.#currentExecutionPromise = this.#currentExecutionPromise
98
  .then(() => {
99
+ return this.#executeAction(actionId, isStreaming);
100
  })
101
  .catch((error) => {
102
  console.error('Action failed:', error);
103
  });
104
  }
105
 
106
+ async #executeAction(actionId: string, isStreaming: boolean = false) {
107
  const action = this.actions.get()[actionId];
108
 
109
  this.#updateAction(actionId, { status: 'running' });
 
118
  await this.#runFileAction(action);
119
  break;
120
  }
121
+ case 'start': {
122
+ await this.#runStartAction(action)
123
+ break;
124
+ }
125
  }
126
 
127
+ this.#updateAction(actionId, { status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete' });
128
  } catch (error) {
129
  this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
130
+ logger.error(`[${action.type}]:Action failed\n\n`, error);
131
 
132
  // re-throw the error to be caught in the promise chain
133
  throw error;
 
138
  if (action.type !== 'shell') {
139
  unreachable('Expected shell action');
140
  }
141
+ const shell = this.#shellTerminal()
142
+ await shell.ready()
143
+ if (!shell || !shell.terminal || !shell.process) {
144
+ unreachable('Shell terminal not found');
145
+ }
146
+ const resp = await shell.executeCommand(this.runnerId.get(), action.content)
147
+ logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`)
148
+ if (resp?.exitCode != 0) {
149
+ throw new Error("Failed To Execute Shell Command");
150
 
151
+ }
152
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
 
154
+ async #runStartAction(action: ActionState) {
155
+ if (action.type !== 'start') {
156
+ unreachable('Expected shell action');
157
+ }
158
+ if (!this.#shellTerminal) {
159
+ unreachable('Shell terminal not found');
160
+ }
161
+ const shell = this.#shellTerminal()
162
+ await shell.ready()
163
+ if (!shell || !shell.terminal || !shell.process) {
164
+ unreachable('Shell terminal not found');
165
+ }
166
+ const resp = await shell.executeCommand(this.runnerId.get(), action.content)
167
+ logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`)
168
 
169
+ if (resp?.exitCode != 0) {
170
+ throw new Error("Failed To Start Application");
171
+ }
172
+ return resp
173
  }
174
 
175
  async #runFileAction(action: ActionState) {
 
200
  logger.error('Failed to write file\n\n', error);
201
  }
202
  }
 
203
  #updateAction(id: string, newState: ActionStateUpdate) {
204
  const actions = this.actions.get();
205
 
app/lib/runtime/message-parser.ts CHANGED
@@ -28,6 +28,7 @@ export interface ParserCallbacks {
28
  onArtifactOpen?: ArtifactCallback;
29
  onArtifactClose?: ArtifactCallback;
30
  onActionOpen?: ActionCallback;
 
31
  onActionClose?: ActionCallback;
32
  }
33
 
@@ -54,7 +55,7 @@ interface MessageState {
54
  export class StreamingMessageParser {
55
  #messages = new Map<string, MessageState>();
56
 
57
- constructor(private _options: StreamingMessageParserOptions = {}) {}
58
 
59
  parse(messageId: string, input: string) {
60
  let state = this.#messages.get(messageId);
@@ -118,6 +119,21 @@ export class StreamingMessageParser {
118
 
119
  i = closeIndex + ARTIFACT_ACTION_TAG_CLOSE.length;
120
  } else {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  break;
122
  }
123
  } else {
@@ -256,7 +272,7 @@ export class StreamingMessageParser {
256
  }
257
 
258
  (actionAttributes as FileAction).filePath = filePath;
259
- } else if (actionType !== 'shell') {
260
  logger.warn(`Unknown action type '${actionType}'`);
261
  }
262
 
 
28
  onArtifactOpen?: ArtifactCallback;
29
  onArtifactClose?: ArtifactCallback;
30
  onActionOpen?: ActionCallback;
31
+ onActionStream?: ActionCallback;
32
  onActionClose?: ActionCallback;
33
  }
34
 
 
55
  export class StreamingMessageParser {
56
  #messages = new Map<string, MessageState>();
57
 
58
+ constructor(private _options: StreamingMessageParserOptions = {}) { }
59
 
60
  parse(messageId: string, input: string) {
61
  let state = this.#messages.get(messageId);
 
119
 
120
  i = closeIndex + ARTIFACT_ACTION_TAG_CLOSE.length;
121
  } else {
122
+ if ('type' in currentAction && currentAction.type === 'file') {
123
+ let content = input.slice(i);
124
+
125
+ this._options.callbacks?.onActionStream?.({
126
+ artifactId: currentArtifact.id,
127
+ messageId,
128
+ actionId: String(state.actionId - 1),
129
+ action: {
130
+ ...currentAction as FileAction,
131
+ content,
132
+ filePath: currentAction.filePath,
133
+ },
134
+
135
+ });
136
+ }
137
  break;
138
  }
139
  } else {
 
272
  }
273
 
274
  (actionAttributes as FileAction).filePath = filePath;
275
+ } else if (!(['shell', 'start'].includes(actionType))) {
276
  logger.warn(`Unknown action type '${actionType}'`);
277
  }
278
 
app/lib/stores/terminal.ts CHANGED
@@ -1,14 +1,15 @@
1
  import type { WebContainer, WebContainerProcess } from '@webcontainer/api';
2
  import { atom, type WritableAtom } from 'nanostores';
3
  import type { ITerminal } from '~/types/terminal';
4
- import { newShellProcess } from '~/utils/shell';
5
  import { coloredText } from '~/utils/terminal';
6
 
7
  export class TerminalStore {
8
  #webcontainer: Promise<WebContainer>;
9
  #terminals: Array<{ terminal: ITerminal; process: WebContainerProcess }> = [];
 
10
 
11
- showTerminal: WritableAtom<boolean> = import.meta.hot?.data.showTerminal ?? atom(false);
12
 
13
  constructor(webcontainerPromise: Promise<WebContainer>) {
14
  this.#webcontainer = webcontainerPromise;
@@ -17,10 +18,22 @@ export class TerminalStore {
17
  import.meta.hot.data.showTerminal = this.showTerminal;
18
  }
19
  }
 
 
 
20
 
21
  toggleTerminal(value?: boolean) {
22
  this.showTerminal.set(value !== undefined ? value : !this.showTerminal.get());
23
  }
 
 
 
 
 
 
 
 
 
24
 
25
  async attachTerminal(terminal: ITerminal) {
26
  try {
 
1
  import type { WebContainer, WebContainerProcess } from '@webcontainer/api';
2
  import { atom, type WritableAtom } from 'nanostores';
3
  import type { ITerminal } from '~/types/terminal';
4
+ import { newBoltShellProcess, newShellProcess } from '~/utils/shell';
5
  import { coloredText } from '~/utils/terminal';
6
 
7
  export class TerminalStore {
8
  #webcontainer: Promise<WebContainer>;
9
  #terminals: Array<{ terminal: ITerminal; process: WebContainerProcess }> = [];
10
+ #boltTerminal = newBoltShellProcess()
11
 
12
+ showTerminal: WritableAtom<boolean> = import.meta.hot?.data.showTerminal ?? atom(true);
13
 
14
  constructor(webcontainerPromise: Promise<WebContainer>) {
15
  this.#webcontainer = webcontainerPromise;
 
18
  import.meta.hot.data.showTerminal = this.showTerminal;
19
  }
20
  }
21
+ get boltTerminal() {
22
+ return this.#boltTerminal;
23
+ }
24
 
25
  toggleTerminal(value?: boolean) {
26
  this.showTerminal.set(value !== undefined ? value : !this.showTerminal.get());
27
  }
28
+ async attachBoltTerminal(terminal: ITerminal) {
29
+ try {
30
+ let wc = await this.#webcontainer
31
+ await this.#boltTerminal.init(wc, terminal)
32
+ } catch (error: any) {
33
+ terminal.write(coloredText.red('Failed to spawn bolt shell\n\n') + error.message);
34
+ return;
35
+ }
36
+ }
37
 
38
  async attachTerminal(terminal: ITerminal) {
39
  try {
app/lib/stores/workbench.ts CHANGED
@@ -11,7 +11,9 @@ import { PreviewsStore } from './previews';
11
  import { TerminalStore } from './terminal';
12
  import JSZip from 'jszip';
13
  import { saveAs } from 'file-saver';
14
- import { Octokit } from "@octokit/rest";
 
 
15
 
16
  export interface ArtifactState {
17
  id: string;
@@ -39,6 +41,7 @@ export class WorkbenchStore {
39
  unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
40
  modifiedFiles = new Set<string>();
41
  artifactIdList: string[] = [];
 
42
 
43
  constructor() {
44
  if (import.meta.hot) {
@@ -76,6 +79,9 @@ export class WorkbenchStore {
76
  get showTerminal() {
77
  return this.#terminalStore.showTerminal;
78
  }
 
 
 
79
 
80
  toggleTerminal(value?: boolean) {
81
  this.#terminalStore.toggleTerminal(value);
@@ -84,6 +90,10 @@ export class WorkbenchStore {
84
  attachTerminal(terminal: ITerminal) {
85
  this.#terminalStore.attachTerminal(terminal);
86
  }
 
 
 
 
87
 
88
  onTerminalResize(cols: number, rows: number) {
89
  this.#terminalStore.onTerminalResize(cols, rows);
@@ -232,7 +242,7 @@ export class WorkbenchStore {
232
  id,
233
  title,
234
  closed: false,
235
- runner: new ActionRunner(webcontainer),
236
  });
237
  }
238
 
@@ -258,7 +268,7 @@ export class WorkbenchStore {
258
  artifact.runner.addAction(data);
259
  }
260
 
261
- async runAction(data: ActionCallbackData) {
262
  const { messageId } = data;
263
 
264
  const artifact = this.#getArtifact(messageId);
@@ -266,8 +276,29 @@ export class WorkbenchStore {
266
  if (!artifact) {
267
  unreachable('Artifact not found');
268
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
269
 
270
- artifact.runner.runAction(data);
 
 
 
 
 
 
 
 
271
  }
272
 
273
  #getArtifact(id: string) {
@@ -336,24 +367,25 @@ export class WorkbenchStore {
336
  }
337
 
338
  async pushToGitHub(repoName: string, githubUsername: string, ghToken: string) {
339
-
340
  try {
341
  // Get the GitHub auth token from environment variables
342
  const githubToken = ghToken;
343
-
344
  const owner = githubUsername;
345
-
346
  if (!githubToken) {
347
  throw new Error('GitHub token is not set in environment variables');
348
  }
349
-
350
  // Initialize Octokit with the auth token
351
  const octokit = new Octokit({ auth: githubToken });
352
-
353
  // Check if the repository already exists before creating it
354
- let repo
355
  try {
356
- repo = await octokit.repos.get({ owner: owner, repo: repoName });
 
357
  } catch (error) {
358
  if (error instanceof Error && 'status' in error && error.status === 404) {
359
  // Repository doesn't exist, so create a new one
@@ -368,13 +400,13 @@ export class WorkbenchStore {
368
  throw error; // Some other error occurred
369
  }
370
  }
371
-
372
  // Get all files
373
  const files = this.files.get();
374
  if (!files || Object.keys(files).length === 0) {
375
  throw new Error('No files found to push');
376
  }
377
-
378
  // Create blobs for each file
379
  const blobs = await Promise.all(
380
  Object.entries(files).map(async ([filePath, dirent]) => {
@@ -389,13 +421,13 @@ export class WorkbenchStore {
389
  }
390
  })
391
  );
392
-
393
  const validBlobs = blobs.filter(Boolean); // Filter out any undefined blobs
394
-
395
  if (validBlobs.length === 0) {
396
  throw new Error('No valid files to push');
397
  }
398
-
399
  // Get the latest commit SHA (assuming main branch, update dynamically if needed)
400
  const { data: ref } = await octokit.git.getRef({
401
  owner: repo.owner.login,
@@ -403,7 +435,7 @@ export class WorkbenchStore {
403
  ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
404
  });
405
  const latestCommitSha = ref.object.sha;
406
-
407
  // Create a new tree
408
  const { data: newTree } = await octokit.git.createTree({
409
  owner: repo.owner.login,
@@ -416,7 +448,7 @@ export class WorkbenchStore {
416
  sha: blob!.sha,
417
  })),
418
  });
419
-
420
  // Create a new commit
421
  const { data: newCommit } = await octokit.git.createCommit({
422
  owner: repo.owner.login,
@@ -425,7 +457,7 @@ export class WorkbenchStore {
425
  tree: newTree.sha,
426
  parents: [latestCommitSha],
427
  });
428
-
429
  // Update the reference
430
  await octokit.git.updateRef({
431
  owner: repo.owner.login,
@@ -433,7 +465,7 @@ export class WorkbenchStore {
433
  ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
434
  sha: newCommit.sha,
435
  });
436
-
437
  alert(`Repository created and code pushed: ${repo.html_url}`);
438
  } catch (error) {
439
  console.error('Error pushing to GitHub:', error instanceof Error ? error.message : String(error));
 
11
  import { TerminalStore } from './terminal';
12
  import JSZip from 'jszip';
13
  import { saveAs } from 'file-saver';
14
+ import { Octokit, type RestEndpointMethodTypes } from "@octokit/rest";
15
+ import * as nodePath from 'node:path';
16
+ import type { WebContainerProcess } from '@webcontainer/api';
17
 
18
  export interface ArtifactState {
19
  id: string;
 
41
  unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
42
  modifiedFiles = new Set<string>();
43
  artifactIdList: string[] = [];
44
+ #boltTerminal: { terminal: ITerminal; process: WebContainerProcess } | undefined;
45
 
46
  constructor() {
47
  if (import.meta.hot) {
 
79
  get showTerminal() {
80
  return this.#terminalStore.showTerminal;
81
  }
82
+ get boltTerminal() {
83
+ return this.#terminalStore.boltTerminal;
84
+ }
85
 
86
  toggleTerminal(value?: boolean) {
87
  this.#terminalStore.toggleTerminal(value);
 
90
  attachTerminal(terminal: ITerminal) {
91
  this.#terminalStore.attachTerminal(terminal);
92
  }
93
+ attachBoltTerminal(terminal: ITerminal) {
94
+
95
+ this.#terminalStore.attachBoltTerminal(terminal);
96
+ }
97
 
98
  onTerminalResize(cols: number, rows: number) {
99
  this.#terminalStore.onTerminalResize(cols, rows);
 
242
  id,
243
  title,
244
  closed: false,
245
+ runner: new ActionRunner(webcontainer, () => this.boltTerminal),
246
  });
247
  }
248
 
 
268
  artifact.runner.addAction(data);
269
  }
270
 
271
+ async runAction(data: ActionCallbackData, isStreaming: boolean = false) {
272
  const { messageId } = data;
273
 
274
  const artifact = this.#getArtifact(messageId);
 
276
  if (!artifact) {
277
  unreachable('Artifact not found');
278
  }
279
+ if (data.action.type === 'file') {
280
+ let wc = await webcontainer
281
+ const fullPath = nodePath.join(wc.workdir, data.action.filePath);
282
+ if (this.selectedFile.value !== fullPath) {
283
+ this.setSelectedFile(fullPath);
284
+ }
285
+ if (this.currentView.value !== 'code') {
286
+ this.currentView.set('code');
287
+ }
288
+ const doc = this.#editorStore.documents.get()[fullPath];
289
+ if (!doc) {
290
+ await artifact.runner.runAction(data, isStreaming);
291
+ }
292
 
293
+ this.#editorStore.updateFile(fullPath, data.action.content);
294
+
295
+ if (!isStreaming) {
296
+ this.resetCurrentDocument();
297
+ await artifact.runner.runAction(data);
298
+ }
299
+ } else {
300
+ artifact.runner.runAction(data);
301
+ }
302
  }
303
 
304
  #getArtifact(id: string) {
 
367
  }
368
 
369
  async pushToGitHub(repoName: string, githubUsername: string, ghToken: string) {
370
+
371
  try {
372
  // Get the GitHub auth token from environment variables
373
  const githubToken = ghToken;
374
+
375
  const owner = githubUsername;
376
+
377
  if (!githubToken) {
378
  throw new Error('GitHub token is not set in environment variables');
379
  }
380
+
381
  // Initialize Octokit with the auth token
382
  const octokit = new Octokit({ auth: githubToken });
383
+
384
  // Check if the repository already exists before creating it
385
+ let repo: RestEndpointMethodTypes["repos"]["get"]["response"]['data']
386
  try {
387
+ let resp = await octokit.repos.get({ owner: owner, repo: repoName });
388
+ repo = resp.data
389
  } catch (error) {
390
  if (error instanceof Error && 'status' in error && error.status === 404) {
391
  // Repository doesn't exist, so create a new one
 
400
  throw error; // Some other error occurred
401
  }
402
  }
403
+
404
  // Get all files
405
  const files = this.files.get();
406
  if (!files || Object.keys(files).length === 0) {
407
  throw new Error('No files found to push');
408
  }
409
+
410
  // Create blobs for each file
411
  const blobs = await Promise.all(
412
  Object.entries(files).map(async ([filePath, dirent]) => {
 
421
  }
422
  })
423
  );
424
+
425
  const validBlobs = blobs.filter(Boolean); // Filter out any undefined blobs
426
+
427
  if (validBlobs.length === 0) {
428
  throw new Error('No valid files to push');
429
  }
430
+
431
  // Get the latest commit SHA (assuming main branch, update dynamically if needed)
432
  const { data: ref } = await octokit.git.getRef({
433
  owner: repo.owner.login,
 
435
  ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
436
  });
437
  const latestCommitSha = ref.object.sha;
438
+
439
  // Create a new tree
440
  const { data: newTree } = await octokit.git.createTree({
441
  owner: repo.owner.login,
 
448
  sha: blob!.sha,
449
  })),
450
  });
451
+
452
  // Create a new commit
453
  const { data: newCommit } = await octokit.git.createCommit({
454
  owner: repo.owner.login,
 
457
  tree: newTree.sha,
458
  parents: [latestCommitSha],
459
  });
460
+
461
  // Update the reference
462
  await octokit.git.updateRef({
463
  owner: repo.owner.login,
 
465
  ref: `heads/${repo.default_branch || 'main'}`, // Handle dynamic branch
466
  sha: newCommit.sha,
467
  });
468
+
469
  alert(`Repository created and code pushed: ${repo.html_url}`);
470
  } catch (error) {
471
  console.error('Error pushing to GitHub:', error instanceof Error ? error.message : String(error));
app/routes/api.enhancer.ts CHANGED
@@ -2,6 +2,7 @@ import { type ActionFunctionArgs } from '@remix-run/cloudflare';
2
  import { StreamingTextResponse, parseStreamPart } from 'ai';
3
  import { streamText } from '~/lib/.server/llm/stream-text';
4
  import { stripIndents } from '~/utils/stripIndent';
 
5
 
6
  const encoder = new TextEncoder();
7
  const decoder = new TextDecoder();
@@ -11,14 +12,34 @@ export async function action(args: ActionFunctionArgs) {
11
  }
12
 
13
  async function enhancerAction({ context, request }: ActionFunctionArgs) {
14
- const { message } = await request.json<{ message: string }>();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
 
16
  try {
17
  const result = await streamText(
18
  [
19
  {
20
  role: 'user',
21
- content: stripIndents`
22
  I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
23
 
24
  IMPORTANT: Only respond with the improved prompt and nothing else!
@@ -30,28 +51,42 @@ async function enhancerAction({ context, request }: ActionFunctionArgs) {
30
  },
31
  ],
32
  context.cloudflare.env,
 
 
33
  );
34
 
35
  const transformStream = new TransformStream({
36
  transform(chunk, controller) {
37
- const processedChunk = decoder
38
- .decode(chunk)
39
- .split('\n')
40
- .filter((line) => line !== '')
41
- .map(parseStreamPart)
42
- .map((part) => part.value)
43
- .join('');
44
-
45
- controller.enqueue(encoder.encode(processedChunk));
 
 
 
 
 
46
  },
47
  });
48
 
49
  const transformedStream = result.toAIStream().pipeThrough(transformStream);
50
 
51
  return new StreamingTextResponse(transformedStream);
52
- } catch (error) {
53
  console.log(error);
54
 
 
 
 
 
 
 
 
55
  throw new Response(null, {
56
  status: 500,
57
  statusText: 'Internal Server Error',
 
2
  import { StreamingTextResponse, parseStreamPart } from 'ai';
3
  import { streamText } from '~/lib/.server/llm/stream-text';
4
  import { stripIndents } from '~/utils/stripIndent';
5
+ import type { StreamingOptions } from '~/lib/.server/llm/stream-text';
6
 
7
  const encoder = new TextEncoder();
8
  const decoder = new TextDecoder();
 
12
  }
13
 
14
  async function enhancerAction({ context, request }: ActionFunctionArgs) {
15
+ const { message, model, provider, apiKeys } = await request.json<{
16
+ message: string;
17
+ model: string;
18
+ provider: string;
19
+ apiKeys?: Record<string, string>;
20
+ }>();
21
+
22
+ // Validate 'model' and 'provider' fields
23
+ if (!model || typeof model !== 'string') {
24
+ throw new Response('Invalid or missing model', {
25
+ status: 400,
26
+ statusText: 'Bad Request'
27
+ });
28
+ }
29
+
30
+ if (!provider || typeof provider !== 'string') {
31
+ throw new Response('Invalid or missing provider', {
32
+ status: 400,
33
+ statusText: 'Bad Request'
34
+ });
35
+ }
36
 
37
  try {
38
  const result = await streamText(
39
  [
40
  {
41
  role: 'user',
42
+ content: `[Model: ${model}]\n\n[Provider: ${provider}]\n\n` + stripIndents`
43
  I want you to improve the user prompt that is wrapped in \`<original_prompt>\` tags.
44
 
45
  IMPORTANT: Only respond with the improved prompt and nothing else!
 
51
  },
52
  ],
53
  context.cloudflare.env,
54
+ undefined,
55
+ apiKeys
56
  );
57
 
58
  const transformStream = new TransformStream({
59
  transform(chunk, controller) {
60
+ const text = decoder.decode(chunk);
61
+ const lines = text.split('\n').filter(line => line.trim() !== '');
62
+
63
+ for (const line of lines) {
64
+ try {
65
+ const parsed = parseStreamPart(line);
66
+ if (parsed.type === 'text') {
67
+ controller.enqueue(encoder.encode(parsed.value));
68
+ }
69
+ } catch (e) {
70
+ // Skip invalid JSON lines
71
+ console.warn('Failed to parse stream part:', line);
72
+ }
73
+ }
74
  },
75
  });
76
 
77
  const transformedStream = result.toAIStream().pipeThrough(transformStream);
78
 
79
  return new StreamingTextResponse(transformedStream);
80
+ } catch (error: unknown) {
81
  console.log(error);
82
 
83
+ if (error instanceof Error && error.message?.includes('API key')) {
84
+ throw new Response('Invalid or missing API key', {
85
+ status: 401,
86
+ statusText: 'Unauthorized'
87
+ });
88
+ }
89
+
90
  throw new Response(null, {
91
  status: 500,
92
  statusText: 'Internal Server Error',
app/types/actions.ts CHANGED
@@ -13,6 +13,10 @@ export interface ShellAction extends BaseAction {
13
  type: 'shell';
14
  }
15
 
16
- export type BoltAction = FileAction | ShellAction;
 
 
 
 
17
 
18
  export type BoltActionData = BoltAction | BaseAction;
 
13
  type: 'shell';
14
  }
15
 
16
+ export interface StartAction extends BaseAction {
17
+ type: 'start';
18
+ }
19
+
20
+ export type BoltAction = FileAction | ShellAction | StartAction;
21
 
22
  export type BoltActionData = BoltAction | BaseAction;
app/types/terminal.ts CHANGED
@@ -5,4 +5,5 @@ export interface ITerminal {
5
  reset: () => void;
6
  write: (data: string) => void;
7
  onData: (cb: (data: string) => void) => void;
 
8
  }
 
5
  reset: () => void;
6
  write: (data: string) => void;
7
  onData: (cb: (data: string) => void) => void;
8
+ input: (data: string) => void;
9
  }
app/utils/shell.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { WebContainer } from '@webcontainer/api';
2
  import type { ITerminal } from '~/types/terminal';
3
  import { withResolvers } from './promises';
 
4
 
5
  export async function newShellProcess(webcontainer: WebContainer, terminal: ITerminal) {
6
  const args: string[] = [];
@@ -19,7 +20,6 @@ export async function newShellProcess(webcontainer: WebContainer, terminal: ITer
19
  const jshReady = withResolvers<void>();
20
 
21
  let isInteractive = false;
22
-
23
  output.pipeTo(
24
  new WritableStream({
25
  write(data) {
@@ -40,6 +40,8 @@ export async function newShellProcess(webcontainer: WebContainer, terminal: ITer
40
  );
41
 
42
  terminal.onData((data) => {
 
 
43
  if (isInteractive) {
44
  input.write(data);
45
  }
@@ -49,3 +51,145 @@ export async function newShellProcess(webcontainer: WebContainer, terminal: ITer
49
 
50
  return process;
51
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { WebContainer, WebContainerProcess } from '@webcontainer/api';
2
  import type { ITerminal } from '~/types/terminal';
3
  import { withResolvers } from './promises';
4
+ import { atom } from 'nanostores';
5
 
6
  export async function newShellProcess(webcontainer: WebContainer, terminal: ITerminal) {
7
  const args: string[] = [];
 
20
  const jshReady = withResolvers<void>();
21
 
22
  let isInteractive = false;
 
23
  output.pipeTo(
24
  new WritableStream({
25
  write(data) {
 
40
  );
41
 
42
  terminal.onData((data) => {
43
+ // console.log('terminal onData', { data, isInteractive });
44
+
45
  if (isInteractive) {
46
  input.write(data);
47
  }
 
51
 
52
  return process;
53
  }
54
+
55
+
56
+
57
+ export class BoltShell {
58
+ #initialized: (() => void) | undefined
59
+ #readyPromise: Promise<void>
60
+ #webcontainer: WebContainer | undefined
61
+ #terminal: ITerminal | undefined
62
+ #process: WebContainerProcess | undefined
63
+ executionState = atom<{ sessionId: string, active: boolean, executionPrms?: Promise<any> } | undefined>()
64
+ #outputStream: ReadableStreamDefaultReader<string> | undefined
65
+ #shellInputStream: WritableStreamDefaultWriter<string> | undefined
66
+ constructor() {
67
+ this.#readyPromise = new Promise((resolve) => {
68
+ this.#initialized = resolve
69
+ })
70
+ }
71
+ ready() {
72
+ return this.#readyPromise;
73
+ }
74
+ async init(webcontainer: WebContainer, terminal: ITerminal) {
75
+ this.#webcontainer = webcontainer
76
+ this.#terminal = terminal
77
+ let callback = (data: string) => {
78
+ console.log(data)
79
+ }
80
+ let { process, output } = await this.newBoltShellProcess(webcontainer, terminal)
81
+ this.#process = process
82
+ this.#outputStream = output.getReader()
83
+ await this.waitTillOscCode('interactive')
84
+ this.#initialized?.()
85
+ }
86
+ get terminal() {
87
+ return this.#terminal
88
+ }
89
+ get process() {
90
+ return this.#process
91
+ }
92
+ async executeCommand(sessionId: string, command: string) {
93
+ if (!this.process || !this.terminal) {
94
+ return
95
+ }
96
+ let state = this.executionState.get()
97
+
98
+ //interrupt the current execution
99
+ // this.#shellInputStream?.write('\x03');
100
+ this.terminal.input('\x03');
101
+ if (state && state.executionPrms) {
102
+ await state.executionPrms
103
+ }
104
+ //start a new execution
105
+ this.terminal.input(command.trim() + '\n');
106
+
107
+ //wait for the execution to finish
108
+ let executionPrms = this.getCurrentExecutionResult()
109
+ this.executionState.set({ sessionId, active: true, executionPrms })
110
+
111
+ let resp = await executionPrms
112
+ this.executionState.set({ sessionId, active: false })
113
+ return resp
114
+
115
+ }
116
+ async newBoltShellProcess(webcontainer: WebContainer, terminal: ITerminal) {
117
+ const args: string[] = [];
118
+
119
+ // we spawn a JSH process with a fallback cols and rows in case the process is not attached yet to a visible terminal
120
+ const process = await webcontainer.spawn('/bin/jsh', ['--osc', ...args], {
121
+ terminal: {
122
+ cols: terminal.cols ?? 80,
123
+ rows: terminal.rows ?? 15,
124
+ },
125
+ });
126
+
127
+ const input = process.input.getWriter();
128
+ this.#shellInputStream = input;
129
+ const [internalOutput, terminalOutput] = process.output.tee();
130
+
131
+ const jshReady = withResolvers<void>();
132
+
133
+ let isInteractive = false;
134
+ terminalOutput.pipeTo(
135
+ new WritableStream({
136
+ write(data) {
137
+ if (!isInteractive) {
138
+ const [, osc] = data.match(/\x1b\]654;([^\x07]+)\x07/) || [];
139
+
140
+ if (osc === 'interactive') {
141
+ // wait until we see the interactive OSC
142
+ isInteractive = true;
143
+
144
+ jshReady.resolve();
145
+ }
146
+ }
147
+
148
+ terminal.write(data);
149
+ },
150
+ }),
151
+ );
152
+
153
+ terminal.onData((data) => {
154
+ // console.log('terminal onData', { data, isInteractive });
155
+
156
+ if (isInteractive) {
157
+ input.write(data);
158
+ }
159
+ });
160
+
161
+ await jshReady.promise;
162
+
163
+ return { process, output: internalOutput };
164
+ }
165
+ async getCurrentExecutionResult() {
166
+ let { output, exitCode } = await this.waitTillOscCode('exit')
167
+ return { output, exitCode };
168
+ }
169
+ async waitTillOscCode(waitCode: string) {
170
+ let fullOutput = '';
171
+ let exitCode: number = 0;
172
+ if (!this.#outputStream) return { output: fullOutput, exitCode };
173
+ let tappedStream = this.#outputStream
174
+
175
+ while (true) {
176
+ const { value, done } = await tappedStream.read();
177
+ if (done) break;
178
+ const text = value || '';
179
+ fullOutput += text;
180
+
181
+ // Check if command completion signal with exit code
182
+ const [, osc, , pid, code] = text.match(/\x1b\]654;([^\x07=]+)=?((-?\d+):(\d+))?\x07/) || [];
183
+ if (osc === 'exit') {
184
+ exitCode = parseInt(code, 10);
185
+ }
186
+ if (osc === waitCode) {
187
+ break;
188
+ }
189
+ }
190
+ return { output: fullOutput, exitCode };
191
+ }
192
+ }
193
+ export function newBoltShellProcess() {
194
+ return new BoltShell();
195
+ }
package.json CHANGED
@@ -16,7 +16,7 @@
16
  "start": "bindings=$(./bindings.sh) && wrangler pages dev ./build/client $bindings",
17
  "dockerstart": "bindings=$(./bindings.sh) && wrangler pages dev ./build/client $bindings --ip 0.0.0.0 --port 5173 --no-show-interactive-dev-session",
18
  "dockerrun": "docker run -it -d --name bolt-ai-live -p 5173:5173 --env-file .env.local bolt-ai",
19
- "dockerbuild:prod": "docker build -t bolt-ai:production bolt-ai:latest --target bolt-ai-production .",
20
  "dockerbuild": "docker build -t bolt-ai:development -t bolt-ai:latest --target bolt-ai-development .",
21
  "typecheck": "tsc",
22
  "typegen": "wrangler types",
@@ -117,5 +117,5 @@
117
  "resolutions": {
118
  "@typescript-eslint/utils": "^8.0.0-alpha.30"
119
  },
120
- "packageManager": "pnpm@9.12.2+sha512.22721b3a11f81661ae1ec68ce1a7b879425a1ca5b991c975b074ac220b187ce56c708fe5db69f4c962c989452eee76c82877f4ee80f474cebd61ee13461b6228"
121
  }
 
16
  "start": "bindings=$(./bindings.sh) && wrangler pages dev ./build/client $bindings",
17
  "dockerstart": "bindings=$(./bindings.sh) && wrangler pages dev ./build/client $bindings --ip 0.0.0.0 --port 5173 --no-show-interactive-dev-session",
18
  "dockerrun": "docker run -it -d --name bolt-ai-live -p 5173:5173 --env-file .env.local bolt-ai",
19
+ "dockerbuild:prod": "docker build -t bolt-ai:production -t bolt-ai:latest --target bolt-ai-production .",
20
  "dockerbuild": "docker build -t bolt-ai:development -t bolt-ai:latest --target bolt-ai-development .",
21
  "typecheck": "tsc",
22
  "typegen": "wrangler types",
 
117
  "resolutions": {
118
  "@typescript-eslint/utils": "^8.0.0-alpha.30"
119
  },
120
+ "packageManager": "pnpm@9.4.0"
121
  }