codacus commited on
Commit
d327cfe
·
1 Parent(s): 42bde1c

feat: added terminal error capturing and automated fix prompt

Browse files
app/commit.json CHANGED
@@ -1 +1 @@
1
- { "commit": "1e72d52278730f7d22448be9d5cf2daf12559486", "version": "0.0.2" }
 
1
+ { "commit": "42bde1cae43de887a6bc5a72f6352a63f65677e6" }
app/components/chat/BaseChat.tsx CHANGED
@@ -28,6 +28,8 @@ import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
28
  import type { IProviderSetting, ProviderInfo } from '~/types/model';
29
  import { ScreenshotStateManager } from './ScreenshotStateManager';
30
  import { toast } from 'react-toastify';
 
 
31
 
32
  const TEXTAREA_MIN_HEIGHT = 76;
33
 
@@ -58,6 +60,8 @@ interface BaseChatProps {
58
  setUploadedFiles?: (files: File[]) => void;
59
  imageDataList?: string[];
60
  setImageDataList?: (dataList: string[]) => void;
 
 
61
  }
62
 
63
  export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
@@ -89,6 +93,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
89
  imageDataList = [],
90
  setImageDataList,
91
  messages,
 
 
92
  },
93
  ref,
94
  ) => {
@@ -313,226 +319,247 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
313
  }}
314
  </ClientOnly>
315
  <div
316
- className={classNames(
317
- 'bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt mb-6',
318
- {
319
- 'sticky bottom-2': chatStarted,
320
- },
321
- )}
322
  >
323
- <svg className={classNames(styles.PromptEffectContainer)}>
324
- <defs>
325
- <linearGradient
326
- id="line-gradient"
327
- x1="20%"
328
- y1="0%"
329
- x2="-14%"
330
- y2="10%"
331
- gradientUnits="userSpaceOnUse"
332
- gradientTransform="rotate(-45)"
333
- >
334
- <stop offset="0%" stopColor="#b44aff" stopOpacity="0%"></stop>
335
- <stop offset="40%" stopColor="#b44aff" stopOpacity="80%"></stop>
336
- <stop offset="50%" stopColor="#b44aff" stopOpacity="80%"></stop>
337
- <stop offset="100%" stopColor="#b44aff" stopOpacity="0%"></stop>
338
- </linearGradient>
339
- <linearGradient id="shine-gradient">
340
- <stop offset="0%" stopColor="white" stopOpacity="0%"></stop>
341
- <stop offset="40%" stopColor="#ffffff" stopOpacity="80%"></stop>
342
- <stop offset="50%" stopColor="#ffffff" stopOpacity="80%"></stop>
343
- <stop offset="100%" stopColor="white" stopOpacity="0%"></stop>
344
- </linearGradient>
345
- </defs>
346
- <rect className={classNames(styles.PromptEffectLine)} pathLength="100" strokeLinecap="round"></rect>
347
- <rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect>
348
- </svg>
349
- <div>
350
- <div className={isModelSettingsCollapsed ? 'hidden' : ''}>
351
- <ModelSelector
352
- key={provider?.name + ':' + modelList.length}
353
- model={model}
354
- setModel={setModel}
355
- modelList={modelList}
356
- provider={provider}
357
- setProvider={setProvider}
358
- providerList={providerList || PROVIDER_LIST}
359
- apiKeys={apiKeys}
360
- />
361
- {(providerList || []).length > 0 && provider && (
362
- <APIKeyManager
363
- provider={provider}
364
- apiKey={apiKeys[provider.name] || ''}
365
- setApiKey={(key) => {
366
- const newApiKeys = { ...apiKeys, [provider.name]: key };
367
- setApiKeys(newApiKeys);
368
- Cookies.set('apiKeys', JSON.stringify(newApiKeys));
369
- }}
370
- />
371
- )}
372
- </div>
373
- </div>
374
- <FilePreview
375
- files={uploadedFiles}
376
- imageDataList={imageDataList}
377
- onRemove={(index) => {
378
- setUploadedFiles?.(uploadedFiles.filter((_, i) => i !== index));
379
- setImageDataList?.(imageDataList.filter((_, i) => i !== index));
380
- }}
381
- />
382
- <ClientOnly>
383
- {() => (
384
- <ScreenshotStateManager
385
- setUploadedFiles={setUploadedFiles}
386
- setImageDataList={setImageDataList}
387
- uploadedFiles={uploadedFiles}
388
- imageDataList={imageDataList}
389
  />
390
  )}
391
- </ClientOnly>
392
  <div
393
  className={classNames(
394
- 'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
 
 
 
 
 
 
395
  )}
396
  >
397
- <textarea
398
- ref={textareaRef}
399
- className={classNames(
400
- 'w-full pl-4 pt-4 pr-16 outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',
401
- 'transition-all duration-200',
402
- 'hover:border-bolt-elements-focus',
403
- )}
404
- onDragEnter={(e) => {
405
- e.preventDefault();
406
- e.currentTarget.style.border = '2px solid #1488fc';
407
- }}
408
- onDragOver={(e) => {
409
- e.preventDefault();
410
- e.currentTarget.style.border = '2px solid #1488fc';
411
- }}
412
- onDragLeave={(e) => {
413
- e.preventDefault();
414
- e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
415
- }}
416
- onDrop={(e) => {
417
- e.preventDefault();
418
- e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
419
-
420
- const files = Array.from(e.dataTransfer.files);
421
- files.forEach((file) => {
422
- if (file.type.startsWith('image/')) {
423
- const reader = new FileReader();
424
-
425
- reader.onload = (e) => {
426
- const base64Image = e.target?.result as string;
427
- setUploadedFiles?.([...uploadedFiles, file]);
428
- setImageDataList?.([...imageDataList, base64Image]);
429
- };
430
- reader.readAsDataURL(file);
431
- }
432
- });
433
- }}
434
- onKeyDown={(event) => {
435
- if (event.key === 'Enter') {
436
- if (event.shiftKey) {
437
- return;
438
- }
439
-
440
- event.preventDefault();
441
-
442
- if (isStreaming) {
443
- handleStop?.();
444
- return;
445
- }
446
-
447
- // ignore if using input method engine
448
- if (event.nativeEvent.isComposing) {
449
- return;
450
- }
451
-
452
- handleSendMessage?.(event);
453
- }
454
- }}
455
- value={input}
456
- onChange={(event) => {
457
- handleInputChange?.(event);
458
- }}
459
- onPaste={handlePaste}
460
- style={{
461
- minHeight: TEXTAREA_MIN_HEIGHT,
462
- maxHeight: TEXTAREA_MAX_HEIGHT,
463
  }}
464
- placeholder="How can Bolt help you today?"
465
- translate="no"
466
  />
467
  <ClientOnly>
468
  {() => (
469
- <SendButton
470
- show={input.length > 0 || isStreaming || uploadedFiles.length > 0}
471
- isStreaming={isStreaming}
472
- disabled={!providerList || providerList.length === 0}
473
- onClick={(event) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
474
  if (isStreaming) {
475
  handleStop?.();
476
  return;
477
  }
478
 
479
- if (input.length > 0 || uploadedFiles.length > 0) {
480
- handleSendMessage?.(event);
 
481
  }
482
- }}
483
- />
484
- )}
485
- </ClientOnly>
486
- <div className="flex justify-between items-center text-sm p-4 pt-2">
487
- <div className="flex gap-1 items-center">
488
- <IconButton title="Upload file" className="transition-all" onClick={() => handleFileUpload()}>
489
- <div className="i-ph:paperclip text-xl"></div>
490
- </IconButton>
491
- <IconButton
492
- title="Enhance prompt"
493
- disabled={input.length === 0 || enhancingPrompt}
494
- className={classNames('transition-all', enhancingPrompt ? 'opacity-100' : '')}
495
- onClick={() => {
496
- enhancePrompt?.();
497
- toast.success('Prompt enhanced!');
498
- }}
499
- >
500
- {enhancingPrompt ? (
501
- <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
502
- ) : (
503
- <div className="i-bolt:stars text-xl"></div>
504
- )}
505
- </IconButton>
506
-
507
- <SpeechRecognitionButton
508
- isListening={isListening}
509
- onStart={startListening}
510
- onStop={stopListening}
511
- disabled={isStreaming}
512
- />
513
- {chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={exportChat} />}</ClientOnly>}
514
- <IconButton
515
- title="Model Settings"
516
- className={classNames('transition-all flex items-center gap-1', {
517
- 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent':
518
- isModelSettingsCollapsed,
519
- 'bg-bolt-elements-item-backgroundDefault text-bolt-elements-item-contentDefault':
520
- !isModelSettingsCollapsed,
521
- })}
522
- onClick={() => setIsModelSettingsCollapsed(!isModelSettingsCollapsed)}
523
- disabled={!providerList || providerList.length === 0}
524
- >
525
- <div className={`i-ph:caret-${isModelSettingsCollapsed ? 'right' : 'down'} text-lg`} />
526
- {isModelSettingsCollapsed ? <span className="text-xs">{model}</span> : <span />}
527
- </IconButton>
528
- </div>
529
- {input.length > 3 ? (
530
- <div className="text-xs text-bolt-elements-textTertiary">
531
- Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> +{' '}
532
- <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> a
533
- new line
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
534
  </div>
535
- ) : null}
 
 
 
 
 
 
 
536
  </div>
537
  </div>
538
  </div>
 
28
  import type { IProviderSetting, ProviderInfo } from '~/types/model';
29
  import { ScreenshotStateManager } from './ScreenshotStateManager';
30
  import { toast } from 'react-toastify';
31
+ import type { ActionAlert } from '~/types/actions';
32
+ import ChatAlert from './ChatAlert';
33
 
34
  const TEXTAREA_MIN_HEIGHT = 76;
35
 
 
60
  setUploadedFiles?: (files: File[]) => void;
61
  imageDataList?: string[];
62
  setImageDataList?: (dataList: string[]) => void;
63
+ actionAlert?: ActionAlert;
64
+ clearAlert?: () => void;
65
  }
66
 
67
  export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
 
93
  imageDataList = [],
94
  setImageDataList,
95
  messages,
96
+ actionAlert,
97
+ clearAlert,
98
  },
99
  ref,
100
  ) => {
 
319
  }}
320
  </ClientOnly>
321
  <div
322
+ className={classNames('flex flex-col gap-4 chatWithContainer z-prompt mb-6', {
323
+ 'sticky bottom-2': chatStarted,
324
+ })}
 
 
 
325
  >
326
+ <div className="bg-bolt-elements-background-depth-2">
327
+ {actionAlert && (
328
+ <ChatAlert
329
+ alert={actionAlert}
330
+ clearAlert={() => clearAlert?.()}
331
+ postMessage={(message) => {
332
+ sendMessage?.({} as any, message);
333
+ clearAlert?.();
334
+ }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
335
  />
336
  )}
337
+ </div>
338
  <div
339
  className={classNames(
340
+ 'bg-bolt-elements-background-depth-2 p-3 rounded-lg border border-bolt-elements-borderColor relative w-full max-w-chat mx-auto z-prompt',
341
+
342
+ /*
343
+ * {
344
+ * 'sticky bottom-2': chatStarted,
345
+ * },
346
+ */
347
  )}
348
  >
349
+ <svg className={classNames(styles.PromptEffectContainer)}>
350
+ <defs>
351
+ <linearGradient
352
+ id="line-gradient"
353
+ x1="20%"
354
+ y1="0%"
355
+ x2="-14%"
356
+ y2="10%"
357
+ gradientUnits="userSpaceOnUse"
358
+ gradientTransform="rotate(-45)"
359
+ >
360
+ <stop offset="0%" stopColor="#b44aff" stopOpacity="0%"></stop>
361
+ <stop offset="40%" stopColor="#b44aff" stopOpacity="80%"></stop>
362
+ <stop offset="50%" stopColor="#b44aff" stopOpacity="80%"></stop>
363
+ <stop offset="100%" stopColor="#b44aff" stopOpacity="0%"></stop>
364
+ </linearGradient>
365
+ <linearGradient id="shine-gradient">
366
+ <stop offset="0%" stopColor="white" stopOpacity="0%"></stop>
367
+ <stop offset="40%" stopColor="#ffffff" stopOpacity="80%"></stop>
368
+ <stop offset="50%" stopColor="#ffffff" stopOpacity="80%"></stop>
369
+ <stop offset="100%" stopColor="white" stopOpacity="0%"></stop>
370
+ </linearGradient>
371
+ </defs>
372
+ <rect className={classNames(styles.PromptEffectLine)} pathLength="100" strokeLinecap="round"></rect>
373
+ <rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect>
374
+ </svg>
375
+ <div>
376
+ <div className={isModelSettingsCollapsed ? 'hidden' : ''}>
377
+ <ModelSelector
378
+ key={provider?.name + ':' + modelList.length}
379
+ model={model}
380
+ setModel={setModel}
381
+ modelList={modelList}
382
+ provider={provider}
383
+ setProvider={setProvider}
384
+ providerList={providerList || PROVIDER_LIST}
385
+ apiKeys={apiKeys}
386
+ />
387
+ {(providerList || []).length > 0 && provider && (
388
+ <APIKeyManager
389
+ provider={provider}
390
+ apiKey={apiKeys[provider.name] || ''}
391
+ setApiKey={(key) => {
392
+ const newApiKeys = { ...apiKeys, [provider.name]: key };
393
+ setApiKeys(newApiKeys);
394
+ Cookies.set('apiKeys', JSON.stringify(newApiKeys));
395
+ }}
396
+ />
397
+ )}
398
+ </div>
399
+ </div>
400
+ <FilePreview
401
+ files={uploadedFiles}
402
+ imageDataList={imageDataList}
403
+ onRemove={(index) => {
404
+ setUploadedFiles?.(uploadedFiles.filter((_, i) => i !== index));
405
+ setImageDataList?.(imageDataList.filter((_, i) => i !== index));
 
 
 
 
 
 
 
 
 
406
  }}
 
 
407
  />
408
  <ClientOnly>
409
  {() => (
410
+ <ScreenshotStateManager
411
+ setUploadedFiles={setUploadedFiles}
412
+ setImageDataList={setImageDataList}
413
+ uploadedFiles={uploadedFiles}
414
+ imageDataList={imageDataList}
415
+ />
416
+ )}
417
+ </ClientOnly>
418
+ <div
419
+ className={classNames(
420
+ 'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
421
+ )}
422
+ >
423
+ <textarea
424
+ ref={textareaRef}
425
+ className={classNames(
426
+ 'w-full pl-4 pt-4 pr-16 outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',
427
+ 'transition-all duration-200',
428
+ 'hover:border-bolt-elements-focus',
429
+ )}
430
+ onDragEnter={(e) => {
431
+ e.preventDefault();
432
+ e.currentTarget.style.border = '2px solid #1488fc';
433
+ }}
434
+ onDragOver={(e) => {
435
+ e.preventDefault();
436
+ e.currentTarget.style.border = '2px solid #1488fc';
437
+ }}
438
+ onDragLeave={(e) => {
439
+ e.preventDefault();
440
+ e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
441
+ }}
442
+ onDrop={(e) => {
443
+ e.preventDefault();
444
+ e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
445
+
446
+ const files = Array.from(e.dataTransfer.files);
447
+ files.forEach((file) => {
448
+ if (file.type.startsWith('image/')) {
449
+ const reader = new FileReader();
450
+
451
+ reader.onload = (e) => {
452
+ const base64Image = e.target?.result as string;
453
+ setUploadedFiles?.([...uploadedFiles, file]);
454
+ setImageDataList?.([...imageDataList, base64Image]);
455
+ };
456
+ reader.readAsDataURL(file);
457
+ }
458
+ });
459
+ }}
460
+ onKeyDown={(event) => {
461
+ if (event.key === 'Enter') {
462
+ if (event.shiftKey) {
463
+ return;
464
+ }
465
+
466
+ event.preventDefault();
467
+
468
  if (isStreaming) {
469
  handleStop?.();
470
  return;
471
  }
472
 
473
+ // ignore if using input method engine
474
+ if (event.nativeEvent.isComposing) {
475
+ return;
476
  }
477
+
478
+ handleSendMessage?.(event);
479
+ }
480
+ }}
481
+ value={input}
482
+ onChange={(event) => {
483
+ handleInputChange?.(event);
484
+ }}
485
+ onPaste={handlePaste}
486
+ style={{
487
+ minHeight: TEXTAREA_MIN_HEIGHT,
488
+ maxHeight: TEXTAREA_MAX_HEIGHT,
489
+ }}
490
+ placeholder="How can Bolt help you today?"
491
+ translate="no"
492
+ />
493
+ <ClientOnly>
494
+ {() => (
495
+ <SendButton
496
+ show={input.length > 0 || isStreaming || uploadedFiles.length > 0}
497
+ isStreaming={isStreaming}
498
+ disabled={!providerList || providerList.length === 0}
499
+ onClick={(event) => {
500
+ if (isStreaming) {
501
+ handleStop?.();
502
+ return;
503
+ }
504
+
505
+ if (input.length > 0 || uploadedFiles.length > 0) {
506
+ handleSendMessage?.(event);
507
+ }
508
+ }}
509
+ />
510
+ )}
511
+ </ClientOnly>
512
+ <div className="flex justify-between items-center text-sm p-4 pt-2">
513
+ <div className="flex gap-1 items-center">
514
+ <IconButton title="Upload file" className="transition-all" onClick={() => handleFileUpload()}>
515
+ <div className="i-ph:paperclip text-xl"></div>
516
+ </IconButton>
517
+ <IconButton
518
+ title="Enhance prompt"
519
+ disabled={input.length === 0 || enhancingPrompt}
520
+ className={classNames('transition-all', enhancingPrompt ? 'opacity-100' : '')}
521
+ onClick={() => {
522
+ enhancePrompt?.();
523
+ toast.success('Prompt enhanced!');
524
+ }}
525
+ >
526
+ {enhancingPrompt ? (
527
+ <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
528
+ ) : (
529
+ <div className="i-bolt:stars text-xl"></div>
530
+ )}
531
+ </IconButton>
532
+
533
+ <SpeechRecognitionButton
534
+ isListening={isListening}
535
+ onStart={startListening}
536
+ onStop={stopListening}
537
+ disabled={isStreaming}
538
+ />
539
+ {chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={exportChat} />}</ClientOnly>}
540
+ <IconButton
541
+ title="Model Settings"
542
+ className={classNames('transition-all flex items-center gap-1', {
543
+ 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent':
544
+ isModelSettingsCollapsed,
545
+ 'bg-bolt-elements-item-backgroundDefault text-bolt-elements-item-contentDefault':
546
+ !isModelSettingsCollapsed,
547
+ })}
548
+ onClick={() => setIsModelSettingsCollapsed(!isModelSettingsCollapsed)}
549
+ disabled={!providerList || providerList.length === 0}
550
+ >
551
+ <div className={`i-ph:caret-${isModelSettingsCollapsed ? 'right' : 'down'} text-lg`} />
552
+ {isModelSettingsCollapsed ? <span className="text-xs">{model}</span> : <span />}
553
+ </IconButton>
554
  </div>
555
+ {input.length > 3 ? (
556
+ <div className="text-xs text-bolt-elements-textTertiary">
557
+ Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd>{' '}
558
+ + <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd>{' '}
559
+ a new line
560
+ </div>
561
+ ) : null}
562
+ </div>
563
  </div>
564
  </div>
565
  </div>
app/components/chat/Chat.client.tsx CHANGED
@@ -95,6 +95,7 @@ export const ChatImpl = memo(
95
  const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
96
  const [searchParams, setSearchParams] = useSearchParams();
97
  const files = useStore(workbenchStore.files);
 
98
  const { activeProviders, promptId } = useSettings();
99
 
100
  const [model, setModel] = useState(() => {
@@ -387,6 +388,8 @@ export const ChatImpl = memo(
387
  setUploadedFiles={setUploadedFiles}
388
  imageDataList={imageDataList}
389
  setImageDataList={setImageDataList}
 
 
390
  />
391
  );
392
  },
 
95
  const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
96
  const [searchParams, setSearchParams] = useSearchParams();
97
  const files = useStore(workbenchStore.files);
98
+ const actionAlert = useStore(workbenchStore.alert);
99
  const { activeProviders, promptId } = useSettings();
100
 
101
  const [model, setModel] = useState(() => {
 
388
  setUploadedFiles={setUploadedFiles}
389
  imageDataList={imageDataList}
390
  setImageDataList={setImageDataList}
391
+ actionAlert={actionAlert}
392
+ clearAlert={() => workbenchStore.clearAlert()}
393
  />
394
  );
395
  },
app/components/chat/ChatAlert.tsx ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { ActionAlert } from '~/types/actions';
2
+ import { classNames } from '~/utils/classNames';
3
+
4
+ interface Props {
5
+ alert: ActionAlert;
6
+ clearAlert: () => void;
7
+ postMessage: (message: string) => void;
8
+ }
9
+
10
+ export default function ChatAlert({ alert, clearAlert, postMessage }: Props) {
11
+ const { type, title, description, content } = alert;
12
+
13
+ const iconColor =
14
+ type === 'error' ? 'text-bolt-elements-button-danger-text' : 'text-bolt-elements-button-primary-text';
15
+
16
+ return (
17
+ <div className={`rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2 p-4`}>
18
+ <div className="flex items-start">
19
+ {/* Icon */}
20
+ <div className="flex-shrink-0">
21
+ {type === 'error' ? (
22
+ <div className={`i-ph:x text-xl ${iconColor}`}></div>
23
+ ) : (
24
+ <svg className={`h-5 w-5 ${iconColor}`} viewBox="0 0 20 20" fill="currentColor">
25
+ <path
26
+ fillRule="evenodd"
27
+ d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
28
+ clipRule="evenodd"
29
+ />
30
+ </svg>
31
+ )}
32
+ </div>
33
+
34
+ {/* Content */}
35
+ <div className="ml-3 flex-1">
36
+ <h3 className={`text-sm font-medium text-bolt-elements-textPrimary`}>{title}</h3>
37
+ <div className={`mt-2 text-sm text-bolt-elements-textSecondary`}>
38
+ <p>{description}</p>
39
+ {/* {content && (
40
+ <pre className="mt-2 whitespace-pre-wrap font-mono text-xs bg-white bg-opacity-50 p-2 rounded">
41
+ {content}
42
+ </pre>
43
+ )} */}
44
+ </div>
45
+
46
+ {/* Actions */}
47
+ <div className="mt-4">
48
+ <div className={classNames(' flex gap-2')}>
49
+ {type === 'error' && (
50
+ <button
51
+ onClick={() => postMessage(`*Fix this error on terminal* \n\`\`\`\n${content}\n\`\`\`\n`)}
52
+ className={classNames(
53
+ `px-2 py-1.5 rounded-md text-sm font-medium`,
54
+ 'bg-bolt-elements-button-primary-background',
55
+ 'hover:bg-bolt-elements-button-primary-backgroundHover',
56
+ 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-danger-background',
57
+ 'text-bolt-elements-button-primary-text',
58
+ )}
59
+ >
60
+ Fix Issue
61
+ </button>
62
+ )}
63
+ <button
64
+ onClick={clearAlert}
65
+ className={classNames(
66
+ `px-2 py-1.5 rounded-md text-sm font-medium`,
67
+ 'bg-bolt-elements-button-secondary-background',
68
+ 'hover:bg-bolt-elements-button-secondary-backgroundHover',
69
+ 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-secondary-background',
70
+ 'text-bolt-elements-button-secondary-text',
71
+ )}
72
+ >
73
+ Dismiss
74
+ </button>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+ );
81
+ }
app/lib/runtime/action-runner.ts CHANGED
@@ -1,7 +1,7 @@
1
  import { WebContainer } 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';
@@ -34,16 +34,51 @@ export type ActionStateUpdate =
34
 
35
  type ActionsMap = MapStore<Record<string, ActionState>>;
36
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  export class ActionRunner {
38
  #webcontainer: Promise<WebContainer>;
39
  #currentExecutionPromise: Promise<void> = Promise.resolve();
40
  #shellTerminal: () => BoltShell;
41
  runnerId = atom<string>(`${Date.now()}`);
42
  actions: ActionsMap = map({});
 
43
 
44
- constructor(webcontainerPromise: Promise<WebContainer>, getShellTerminal: () => BoltShell) {
 
 
 
 
45
  this.#webcontainer = webcontainerPromise;
46
  this.#shellTerminal = getShellTerminal;
 
47
  }
48
 
49
  addAction(data: ActionCallbackData) {
@@ -126,7 +161,25 @@ export class ActionRunner {
126
 
127
  this.#runStartAction(action)
128
  .then(() => this.#updateAction(actionId, { status: 'complete' }))
129
- .catch(() => this.#updateAction(actionId, { status: 'failed', error: 'Action failed' }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  /*
132
  * adding a delay to avoid any race condition between 2 start actions
@@ -142,9 +195,24 @@ export class ActionRunner {
142
  status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete',
143
  });
144
  } catch (error) {
 
 
 
 
145
  this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
146
  logger.error(`[${action.type}]:Action failed\n\n`, error);
147
 
 
 
 
 
 
 
 
 
 
 
 
148
  // re-throw the error to be caught in the promise chain
149
  throw error;
150
  }
@@ -162,11 +230,14 @@ export class ActionRunner {
162
  unreachable('Shell terminal not found');
163
  }
164
 
165
- const resp = await shell.executeCommand(this.runnerId.get(), action.content);
 
 
 
166
  logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`);
167
 
168
  if (resp?.exitCode != 0) {
169
- throw new Error('Failed To Execute Shell Command');
170
  }
171
  }
172
 
@@ -186,11 +257,14 @@ export class ActionRunner {
186
  unreachable('Shell terminal not found');
187
  }
188
 
189
- const resp = await shell.executeCommand(this.runnerId.get(), action.content);
 
 
 
190
  logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`);
191
 
192
  if (resp?.exitCode != 0) {
193
- throw new Error('Failed To Start Application');
194
  }
195
 
196
  return resp;
 
1
  import { WebContainer } from '@webcontainer/api';
2
  import { atom, map, type MapStore } from 'nanostores';
3
  import * as nodePath from 'node:path';
4
+ import type { ActionAlert, BoltAction } from '~/types/actions';
5
  import { createScopedLogger } from '~/utils/logger';
6
  import { unreachable } from '~/utils/unreachable';
7
  import type { ActionCallbackData } from './message-parser';
 
34
 
35
  type ActionsMap = MapStore<Record<string, ActionState>>;
36
 
37
+ class ActionCommandError extends Error {
38
+ readonly _output: string;
39
+ readonly _header: string;
40
+
41
+ constructor(message: string, output: string) {
42
+ // Create a formatted message that includes both the error message and output
43
+ const formattedMessage = `Failed To Execute Shell Command: ${message}\n\nOutput:\n${output}`;
44
+ super(formattedMessage);
45
+
46
+ // Set the output separately so it can be accessed programmatically
47
+ this._header = message;
48
+ this._output = output;
49
+
50
+ // Maintain proper prototype chain
51
+ Object.setPrototypeOf(this, ActionCommandError.prototype);
52
+
53
+ // Set the name of the error for better debugging
54
+ this.name = 'ActionCommandError';
55
+ }
56
+
57
+ // Optional: Add a method to get just the terminal output
58
+ get output() {
59
+ return this._output;
60
+ }
61
+ get header() {
62
+ return this._header;
63
+ }
64
+ }
65
+
66
  export class ActionRunner {
67
  #webcontainer: Promise<WebContainer>;
68
  #currentExecutionPromise: Promise<void> = Promise.resolve();
69
  #shellTerminal: () => BoltShell;
70
  runnerId = atom<string>(`${Date.now()}`);
71
  actions: ActionsMap = map({});
72
+ onAlert?: (alert: ActionAlert) => void;
73
 
74
+ constructor(
75
+ webcontainerPromise: Promise<WebContainer>,
76
+ getShellTerminal: () => BoltShell,
77
+ onAlert?: (alert: ActionAlert) => void,
78
+ ) {
79
  this.#webcontainer = webcontainerPromise;
80
  this.#shellTerminal = getShellTerminal;
81
+ this.onAlert = onAlert;
82
  }
83
 
84
  addAction(data: ActionCallbackData) {
 
161
 
162
  this.#runStartAction(action)
163
  .then(() => this.#updateAction(actionId, { status: 'complete' }))
164
+ .catch((err: Error) => {
165
+ if (action.abortSignal.aborted) {
166
+ return;
167
+ }
168
+
169
+ this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
170
+ logger.error(`[${action.type}]:Action failed\n\n`, err);
171
+
172
+ if (!(err instanceof ActionCommandError)) {
173
+ return;
174
+ }
175
+
176
+ this.onAlert?.({
177
+ type: 'error',
178
+ title: 'Dev Server Failed',
179
+ description: err.header,
180
+ content: err.output,
181
+ });
182
+ });
183
 
184
  /*
185
  * adding a delay to avoid any race condition between 2 start actions
 
195
  status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete',
196
  });
197
  } catch (error) {
198
+ if (action.abortSignal.aborted) {
199
+ return;
200
+ }
201
+
202
  this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
203
  logger.error(`[${action.type}]:Action failed\n\n`, error);
204
 
205
+ if (!(error instanceof ActionCommandError)) {
206
+ return;
207
+ }
208
+
209
+ this.onAlert?.({
210
+ type: 'error',
211
+ title: 'Dev Server Failed',
212
+ description: error.header,
213
+ content: error.output,
214
+ });
215
+
216
  // re-throw the error to be caught in the promise chain
217
  throw error;
218
  }
 
230
  unreachable('Shell terminal not found');
231
  }
232
 
233
+ const resp = await shell.executeCommand(this.runnerId.get(), action.content, () => {
234
+ logger.debug(`[${action.type}]:Aborting Action\n\n`, action);
235
+ action.abort();
236
+ });
237
  logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`);
238
 
239
  if (resp?.exitCode != 0) {
240
+ throw new ActionCommandError(`Failed To Execute Shell Command`, resp?.output || 'No Output Available');
241
  }
242
  }
243
 
 
257
  unreachable('Shell terminal not found');
258
  }
259
 
260
+ const resp = await shell.executeCommand(this.runnerId.get(), action.content, () => {
261
+ logger.debug(`[${action.type}]:Aborting Action\n\n`, action);
262
+ action.abort();
263
+ });
264
  logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`);
265
 
266
  if (resp?.exitCode != 0) {
267
+ throw new ActionCommandError('Failed To Start Application', resp?.output || 'No Output Available');
268
  }
269
 
270
  return resp;
app/lib/stores/workbench.ts CHANGED
@@ -17,6 +17,7 @@ import { extractRelativePath } from '~/utils/diff';
17
  import { description } from '~/lib/persistence';
18
  import Cookies from 'js-cookie';
19
  import { createSampler } from '~/utils/sampler';
 
20
 
21
  export interface ArtifactState {
22
  id: string;
@@ -43,6 +44,8 @@ export class WorkbenchStore {
43
  showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false);
44
  currentView: WritableAtom<WorkbenchViewType> = import.meta.hot?.data.currentView ?? atom('code');
45
  unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
 
 
46
  modifiedFiles = new Set<string>();
47
  artifactIdList: string[] = [];
48
  #globalExecutionQueue = Promise.resolve();
@@ -52,6 +55,7 @@ export class WorkbenchStore {
52
  import.meta.hot.data.unsavedFiles = this.unsavedFiles;
53
  import.meta.hot.data.showWorkbench = this.showWorkbench;
54
  import.meta.hot.data.currentView = this.currentView;
 
55
  }
56
  }
57
 
@@ -89,6 +93,12 @@ export class WorkbenchStore {
89
  get boltTerminal() {
90
  return this.#terminalStore.boltTerminal;
91
  }
 
 
 
 
 
 
92
 
93
  toggleTerminal(value?: boolean) {
94
  this.#terminalStore.toggleTerminal(value);
@@ -249,7 +259,11 @@ export class WorkbenchStore {
249
  title,
250
  closed: false,
251
  type,
252
- runner: new ActionRunner(webcontainer, () => this.boltTerminal),
 
 
 
 
253
  });
254
  }
255
 
 
17
  import { description } from '~/lib/persistence';
18
  import Cookies from 'js-cookie';
19
  import { createSampler } from '~/utils/sampler';
20
+ import type { ActionAlert } from '~/types/actions';
21
 
22
  export interface ArtifactState {
23
  id: string;
 
44
  showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false);
45
  currentView: WritableAtom<WorkbenchViewType> = import.meta.hot?.data.currentView ?? atom('code');
46
  unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
47
+ actionAlert: WritableAtom<ActionAlert | undefined> =
48
+ import.meta.hot?.data.unsavedFiles ?? atom<ActionAlert | undefined>(undefined);
49
  modifiedFiles = new Set<string>();
50
  artifactIdList: string[] = [];
51
  #globalExecutionQueue = Promise.resolve();
 
55
  import.meta.hot.data.unsavedFiles = this.unsavedFiles;
56
  import.meta.hot.data.showWorkbench = this.showWorkbench;
57
  import.meta.hot.data.currentView = this.currentView;
58
+ import.meta.hot.data.actionAlert = this.actionAlert;
59
  }
60
  }
61
 
 
93
  get boltTerminal() {
94
  return this.#terminalStore.boltTerminal;
95
  }
96
+ get alert() {
97
+ return this.actionAlert;
98
+ }
99
+ clearAlert() {
100
+ this.actionAlert.set(undefined);
101
+ }
102
 
103
  toggleTerminal(value?: boolean) {
104
  this.#terminalStore.toggleTerminal(value);
 
259
  title,
260
  closed: false,
261
  type,
262
+ runner: new ActionRunner(
263
+ webcontainer,
264
+ () => this.boltTerminal,
265
+ (alert) => this.actionAlert.set(alert),
266
+ ),
267
  });
268
  }
269
 
app/types/actions.ts CHANGED
@@ -20,3 +20,10 @@ export interface StartAction extends BaseAction {
20
  export type BoltAction = FileAction | ShellAction | StartAction;
21
 
22
  export type BoltActionData = BoltAction | BaseAction;
 
 
 
 
 
 
 
 
20
  export type BoltAction = FileAction | ShellAction | StartAction;
21
 
22
  export type BoltActionData = BoltAction | BaseAction;
23
+
24
+ export interface ActionAlert {
25
+ type: 'error' | 'info';
26
+ title: string;
27
+ description: string;
28
+ content: string;
29
+ }
app/utils/shell.ts CHANGED
@@ -60,7 +60,9 @@ export class BoltShell {
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
 
@@ -93,13 +95,17 @@ export class BoltShell {
93
  return this.#process;
94
  }
95
 
96
- async executeCommand(sessionId: string, command: string): Promise<ExecutionResult> {
97
  if (!this.process || !this.terminal) {
98
  return undefined;
99
  }
100
 
101
  const state = this.executionState.get();
102
 
 
 
 
 
103
  /*
104
  * interrupt the current execution
105
  * this.#shellInputStream?.write('\x03');
@@ -115,11 +121,19 @@ export class BoltShell {
115
 
116
  //wait for the execution to finish
117
  const executionPromise = this.getCurrentExecutionResult();
118
- this.executionState.set({ sessionId, active: true, executionPrms: executionPromise });
119
 
120
  const resp = await executionPromise;
121
  this.executionState.set({ sessionId, active: false });
122
 
 
 
 
 
 
 
 
 
123
  return resp;
124
  }
125
 
@@ -215,6 +229,47 @@ export class BoltShell {
215
  }
216
  }
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  export function newBoltShellProcess() {
219
  return new BoltShell();
220
  }
 
60
  #webcontainer: WebContainer | undefined;
61
  #terminal: ITerminal | undefined;
62
  #process: WebContainerProcess | undefined;
63
+ executionState = atom<
64
+ { sessionId: string; active: boolean; executionPrms?: Promise<any>; abort?: () => void } | undefined
65
+ >();
66
  #outputStream: ReadableStreamDefaultReader<string> | undefined;
67
  #shellInputStream: WritableStreamDefaultWriter<string> | undefined;
68
 
 
95
  return this.#process;
96
  }
97
 
98
+ async executeCommand(sessionId: string, command: string, abort?: () => void): Promise<ExecutionResult> {
99
  if (!this.process || !this.terminal) {
100
  return undefined;
101
  }
102
 
103
  const state = this.executionState.get();
104
 
105
+ if (state?.active && state.abort) {
106
+ state.abort();
107
+ }
108
+
109
  /*
110
  * interrupt the current execution
111
  * this.#shellInputStream?.write('\x03');
 
121
 
122
  //wait for the execution to finish
123
  const executionPromise = this.getCurrentExecutionResult();
124
+ this.executionState.set({ sessionId, active: true, executionPrms: executionPromise, abort });
125
 
126
  const resp = await executionPromise;
127
  this.executionState.set({ sessionId, active: false });
128
 
129
+ if (resp) {
130
+ try {
131
+ resp.output = cleanTerminalOutput(resp.output);
132
+ } catch (error) {
133
+ console.log('failed to format terminal output', error);
134
+ }
135
+ }
136
+
137
  return resp;
138
  }
139
 
 
229
  }
230
  }
231
 
232
+ /**
233
+ * Cleans and formats terminal output while preserving structure and paths
234
+ */
235
+ export function cleanTerminalOutput(input: string): string {
236
+ // Step 1: Remove ANSI escape sequences and control characters
237
+ const removeAnsi = input.replace(/\u001b\[[0-9;]*[a-zA-Z]/g, '');
238
+
239
+ // Step 2: Remove terminal control sequences
240
+ const removeControl = removeAnsi
241
+ .replace(/\[\?[0-9;]*[a-zA-Z]/g, '')
242
+ .replace(/\]654;[^\n]*/g, '')
243
+ .replace(/\[[0-9]+[GJ]/g, '');
244
+
245
+ // Step 3: Add newlines at key breakpoints while preserving paths
246
+ const formatOutput = removeControl
247
+ // Preserve prompt line
248
+ .replace(/^([~\/][^\n❯]+)❯/m, '$1\n❯')
249
+ // Add newline before command output indicators
250
+ .replace(/(?<!^|\n)>/g, '\n>')
251
+ // Add newline before error keywords without breaking paths
252
+ .replace(/(?<!^|\n|\w)(error|failed|warning|Error|Failed|Warning):/g, '\n$1:')
253
+ // Add newline before 'at' in stack traces without breaking paths
254
+ .replace(/(?<!^|\n|\/)(at\s+(?!async|sync))/g, '\nat ')
255
+ // Ensure 'at async' stays on same line
256
+ .replace(/\bat\s+async/g, 'at async');
257
+
258
+ // Step 4: Clean up whitespace while preserving intentional spacing
259
+ const cleanSpaces = formatOutput
260
+ .split('\n')
261
+ .map((line) => line.trim())
262
+ .filter((line) => line.length > 0) // Remove empty lines
263
+ .join('\n');
264
+
265
+ // Step 5: Final cleanup
266
+ return cleanSpaces
267
+ .replace(/\n{3,}/g, '\n\n') // Replace multiple newlines with double newlines
268
+ .replace(/:\s+/g, ': ') // Normalize spacing after colons
269
+ .replace(/\s{2,}/g, ' ') // Remove multiple spaces
270
+ .trim();
271
+ }
272
+
273
  export function newBoltShellProcess() {
274
  return new BoltShell();
275
  }