Eduards commited on
Commit
79e7e75
·
unverified ·
2 Parent(s): 1f938fc 13b208d

Merge pull request #797 from thecodacus/terminal-error-detection

Browse files
README.md CHANGED
@@ -61,6 +61,7 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed
61
  - ✅ PromptLibrary to have different variations of prompts for different use cases (@thecodacus)
62
  - ✅ Detect package.json and commands to auto install & run preview for folder and git import (@wonderwhy-er)
63
  - ✅ Selection tool to target changes visually (@emcconnell)
 
64
  - ⬜ **HIGH PRIORITY** - Prevent bolt from rewriting files as often (file locking and diffs)
65
  - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
66
  - ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
 
61
  - ✅ PromptLibrary to have different variations of prompts for different use cases (@thecodacus)
62
  - ✅ Detect package.json and commands to auto install & run preview for folder and git import (@wonderwhy-er)
63
  - ✅ Selection tool to target changes visually (@emcconnell)
64
+ - ✅ Detect terminal Errors and ask bolt to fix it (@thecodacus)
65
  - ⬜ **HIGH PRIORITY** - Prevent bolt from rewriting files as often (file locking and diffs)
66
  - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
67
  - ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call
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
  ) => {
@@ -318,226 +324,247 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
318
  }}
319
  </ClientOnly>
320
  <div
321
- className={classNames(
322
- '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',
323
- {
324
- 'sticky bottom-2': chatStarted,
325
- },
326
- )}
327
  >
328
- <svg className={classNames(styles.PromptEffectContainer)}>
329
- <defs>
330
- <linearGradient
331
- id="line-gradient"
332
- x1="20%"
333
- y1="0%"
334
- x2="-14%"
335
- y2="10%"
336
- gradientUnits="userSpaceOnUse"
337
- gradientTransform="rotate(-45)"
338
- >
339
- <stop offset="0%" stopColor="#b44aff" stopOpacity="0%"></stop>
340
- <stop offset="40%" stopColor="#b44aff" stopOpacity="80%"></stop>
341
- <stop offset="50%" stopColor="#b44aff" stopOpacity="80%"></stop>
342
- <stop offset="100%" stopColor="#b44aff" stopOpacity="0%"></stop>
343
- </linearGradient>
344
- <linearGradient id="shine-gradient">
345
- <stop offset="0%" stopColor="white" stopOpacity="0%"></stop>
346
- <stop offset="40%" stopColor="#ffffff" stopOpacity="80%"></stop>
347
- <stop offset="50%" stopColor="#ffffff" stopOpacity="80%"></stop>
348
- <stop offset="100%" stopColor="white" stopOpacity="0%"></stop>
349
- </linearGradient>
350
- </defs>
351
- <rect className={classNames(styles.PromptEffectLine)} pathLength="100" strokeLinecap="round"></rect>
352
- <rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect>
353
- </svg>
354
- <div>
355
- <div className={isModelSettingsCollapsed ? 'hidden' : ''}>
356
- <ModelSelector
357
- key={provider?.name + ':' + modelList.length}
358
- model={model}
359
- setModel={setModel}
360
- modelList={modelList}
361
- provider={provider}
362
- setProvider={setProvider}
363
- providerList={providerList || (PROVIDER_LIST as ProviderInfo[])}
364
- apiKeys={apiKeys}
365
- />
366
- {(providerList || []).length > 0 && provider && (
367
- <APIKeyManager
368
- provider={provider}
369
- apiKey={apiKeys[provider.name] || ''}
370
- setApiKey={(key) => {
371
- const newApiKeys = { ...apiKeys, [provider.name]: key };
372
- setApiKeys(newApiKeys);
373
- Cookies.set('apiKeys', JSON.stringify(newApiKeys));
374
- }}
375
- />
376
- )}
377
- </div>
378
- </div>
379
- <FilePreview
380
- files={uploadedFiles}
381
- imageDataList={imageDataList}
382
- onRemove={(index) => {
383
- setUploadedFiles?.(uploadedFiles.filter((_, i) => i !== index));
384
- setImageDataList?.(imageDataList.filter((_, i) => i !== index));
385
- }}
386
- />
387
- <ClientOnly>
388
- {() => (
389
- <ScreenshotStateManager
390
- setUploadedFiles={setUploadedFiles}
391
- setImageDataList={setImageDataList}
392
- uploadedFiles={uploadedFiles}
393
- imageDataList={imageDataList}
394
  />
395
  )}
396
- </ClientOnly>
397
  <div
398
  className={classNames(
399
- 'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
 
 
 
 
 
 
400
  )}
401
  >
402
- <textarea
403
- ref={textareaRef}
404
- className={classNames(
405
- 'w-full pl-4 pt-4 pr-16 outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',
406
- 'transition-all duration-200',
407
- 'hover:border-bolt-elements-focus',
408
- )}
409
- onDragEnter={(e) => {
410
- e.preventDefault();
411
- e.currentTarget.style.border = '2px solid #1488fc';
412
- }}
413
- onDragOver={(e) => {
414
- e.preventDefault();
415
- e.currentTarget.style.border = '2px solid #1488fc';
416
- }}
417
- onDragLeave={(e) => {
418
- e.preventDefault();
419
- e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
420
- }}
421
- onDrop={(e) => {
422
- e.preventDefault();
423
- e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
424
-
425
- const files = Array.from(e.dataTransfer.files);
426
- files.forEach((file) => {
427
- if (file.type.startsWith('image/')) {
428
- const reader = new FileReader();
429
-
430
- reader.onload = (e) => {
431
- const base64Image = e.target?.result as string;
432
- setUploadedFiles?.([...uploadedFiles, file]);
433
- setImageDataList?.([...imageDataList, base64Image]);
434
- };
435
- reader.readAsDataURL(file);
436
- }
437
- });
438
- }}
439
- onKeyDown={(event) => {
440
- if (event.key === 'Enter') {
441
- if (event.shiftKey) {
442
- return;
443
- }
444
-
445
- event.preventDefault();
446
-
447
- if (isStreaming) {
448
- handleStop?.();
449
- return;
450
- }
451
-
452
- // ignore if using input method engine
453
- if (event.nativeEvent.isComposing) {
454
- return;
455
- }
456
-
457
- handleSendMessage?.(event);
458
- }
459
- }}
460
- value={input}
461
- onChange={(event) => {
462
- handleInputChange?.(event);
463
- }}
464
- onPaste={handlePaste}
465
- style={{
466
- minHeight: TEXTAREA_MIN_HEIGHT,
467
- maxHeight: TEXTAREA_MAX_HEIGHT,
468
  }}
469
- placeholder="How can Bolt help you today?"
470
- translate="no"
471
  />
472
  <ClientOnly>
473
  {() => (
474
- <SendButton
475
- show={input.length > 0 || isStreaming || uploadedFiles.length > 0}
476
- isStreaming={isStreaming}
477
- disabled={!providerList || providerList.length === 0}
478
- onClick={(event) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
  if (isStreaming) {
480
  handleStop?.();
481
  return;
482
  }
483
 
484
- if (input.length > 0 || uploadedFiles.length > 0) {
485
- handleSendMessage?.(event);
 
486
  }
487
- }}
488
- />
489
- )}
490
- </ClientOnly>
491
- <div className="flex justify-between items-center text-sm p-4 pt-2">
492
- <div className="flex gap-1 items-center">
493
- <IconButton title="Upload file" className="transition-all" onClick={() => handleFileUpload()}>
494
- <div className="i-ph:paperclip text-xl"></div>
495
- </IconButton>
496
- <IconButton
497
- title="Enhance prompt"
498
- disabled={input.length === 0 || enhancingPrompt}
499
- className={classNames('transition-all', enhancingPrompt ? 'opacity-100' : '')}
500
- onClick={() => {
501
- enhancePrompt?.();
502
- toast.success('Prompt enhanced!');
503
- }}
504
- >
505
- {enhancingPrompt ? (
506
- <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
507
- ) : (
508
- <div className="i-bolt:stars text-xl"></div>
509
- )}
510
- </IconButton>
511
-
512
- <SpeechRecognitionButton
513
- isListening={isListening}
514
- onStart={startListening}
515
- onStop={stopListening}
516
- disabled={isStreaming}
517
- />
518
- {chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={exportChat} />}</ClientOnly>}
519
- <IconButton
520
- title="Model Settings"
521
- className={classNames('transition-all flex items-center gap-1', {
522
- 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent':
523
- isModelSettingsCollapsed,
524
- 'bg-bolt-elements-item-backgroundDefault text-bolt-elements-item-contentDefault':
525
- !isModelSettingsCollapsed,
526
- })}
527
- onClick={() => setIsModelSettingsCollapsed(!isModelSettingsCollapsed)}
528
- disabled={!providerList || providerList.length === 0}
529
- >
530
- <div className={`i-ph:caret-${isModelSettingsCollapsed ? 'right' : 'down'} text-lg`} />
531
- {isModelSettingsCollapsed ? <span className="text-xs">{model}</span> : <span />}
532
- </IconButton>
533
- </div>
534
- {input.length > 3 ? (
535
- <div className="text-xs text-bolt-elements-textTertiary">
536
- Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> +{' '}
537
- <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> a
538
- new line
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  </div>
540
- ) : null}
 
 
 
 
 
 
 
541
  </div>
542
  </div>
543
  </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
  ) => {
 
324
  }}
325
  </ClientOnly>
326
  <div
327
+ className={classNames('flex flex-col gap-4 w-full max-w-chat mx-auto z-prompt mb-6', {
328
+ 'sticky bottom-2': chatStarted,
329
+ })}
 
 
 
330
  >
331
+ <div className="bg-bolt-elements-background-depth-2">
332
+ {actionAlert && (
333
+ <ChatAlert
334
+ alert={actionAlert}
335
+ clearAlert={() => clearAlert?.()}
336
+ postMessage={(message) => {
337
+ sendMessage?.({} as any, message);
338
+ clearAlert?.();
339
+ }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
340
  />
341
  )}
342
+ </div>
343
  <div
344
  className={classNames(
345
+ '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',
346
+
347
+ /*
348
+ * {
349
+ * 'sticky bottom-2': chatStarted,
350
+ * },
351
+ */
352
  )}
353
  >
354
+ <svg className={classNames(styles.PromptEffectContainer)}>
355
+ <defs>
356
+ <linearGradient
357
+ id="line-gradient"
358
+ x1="20%"
359
+ y1="0%"
360
+ x2="-14%"
361
+ y2="10%"
362
+ gradientUnits="userSpaceOnUse"
363
+ gradientTransform="rotate(-45)"
364
+ >
365
+ <stop offset="0%" stopColor="#b44aff" stopOpacity="0%"></stop>
366
+ <stop offset="40%" stopColor="#b44aff" stopOpacity="80%"></stop>
367
+ <stop offset="50%" stopColor="#b44aff" stopOpacity="80%"></stop>
368
+ <stop offset="100%" stopColor="#b44aff" stopOpacity="0%"></stop>
369
+ </linearGradient>
370
+ <linearGradient id="shine-gradient">
371
+ <stop offset="0%" stopColor="white" stopOpacity="0%"></stop>
372
+ <stop offset="40%" stopColor="#ffffff" stopOpacity="80%"></stop>
373
+ <stop offset="50%" stopColor="#ffffff" stopOpacity="80%"></stop>
374
+ <stop offset="100%" stopColor="white" stopOpacity="0%"></stop>
375
+ </linearGradient>
376
+ </defs>
377
+ <rect className={classNames(styles.PromptEffectLine)} pathLength="100" strokeLinecap="round"></rect>
378
+ <rect className={classNames(styles.PromptShine)} x="48" y="24" width="70" height="1"></rect>
379
+ </svg>
380
+ <div>
381
+ <div className={isModelSettingsCollapsed ? 'hidden' : ''}>
382
+ <ModelSelector
383
+ key={provider?.name + ':' + modelList.length}
384
+ model={model}
385
+ setModel={setModel}
386
+ modelList={modelList}
387
+ provider={provider}
388
+ setProvider={setProvider}
389
+ providerList={providerList || (PROVIDER_LIST as ProviderInfo[])}
390
+ apiKeys={apiKeys}
391
+ />
392
+ {(providerList || []).length > 0 && provider && (
393
+ <APIKeyManager
394
+ provider={provider}
395
+ apiKey={apiKeys[provider.name] || ''}
396
+ setApiKey={(key) => {
397
+ const newApiKeys = { ...apiKeys, [provider.name]: key };
398
+ setApiKeys(newApiKeys);
399
+ Cookies.set('apiKeys', JSON.stringify(newApiKeys));
400
+ }}
401
+ />
402
+ )}
403
+ </div>
404
+ </div>
405
+ <FilePreview
406
+ files={uploadedFiles}
407
+ imageDataList={imageDataList}
408
+ onRemove={(index) => {
409
+ setUploadedFiles?.(uploadedFiles.filter((_, i) => i !== index));
410
+ setImageDataList?.(imageDataList.filter((_, i) => i !== index));
 
 
 
 
 
 
 
 
 
411
  }}
 
 
412
  />
413
  <ClientOnly>
414
  {() => (
415
+ <ScreenshotStateManager
416
+ setUploadedFiles={setUploadedFiles}
417
+ setImageDataList={setImageDataList}
418
+ uploadedFiles={uploadedFiles}
419
+ imageDataList={imageDataList}
420
+ />
421
+ )}
422
+ </ClientOnly>
423
+ <div
424
+ className={classNames(
425
+ 'relative shadow-xs border border-bolt-elements-borderColor backdrop-blur rounded-lg',
426
+ )}
427
+ >
428
+ <textarea
429
+ ref={textareaRef}
430
+ className={classNames(
431
+ 'w-full pl-4 pt-4 pr-16 outline-none resize-none text-bolt-elements-textPrimary placeholder-bolt-elements-textTertiary bg-transparent text-sm',
432
+ 'transition-all duration-200',
433
+ 'hover:border-bolt-elements-focus',
434
+ )}
435
+ onDragEnter={(e) => {
436
+ e.preventDefault();
437
+ e.currentTarget.style.border = '2px solid #1488fc';
438
+ }}
439
+ onDragOver={(e) => {
440
+ e.preventDefault();
441
+ e.currentTarget.style.border = '2px solid #1488fc';
442
+ }}
443
+ onDragLeave={(e) => {
444
+ e.preventDefault();
445
+ e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
446
+ }}
447
+ onDrop={(e) => {
448
+ e.preventDefault();
449
+ e.currentTarget.style.border = '1px solid var(--bolt-elements-borderColor)';
450
+
451
+ const files = Array.from(e.dataTransfer.files);
452
+ files.forEach((file) => {
453
+ if (file.type.startsWith('image/')) {
454
+ const reader = new FileReader();
455
+
456
+ reader.onload = (e) => {
457
+ const base64Image = e.target?.result as string;
458
+ setUploadedFiles?.([...uploadedFiles, file]);
459
+ setImageDataList?.([...imageDataList, base64Image]);
460
+ };
461
+ reader.readAsDataURL(file);
462
+ }
463
+ });
464
+ }}
465
+ onKeyDown={(event) => {
466
+ if (event.key === 'Enter') {
467
+ if (event.shiftKey) {
468
+ return;
469
+ }
470
+
471
+ event.preventDefault();
472
+
473
  if (isStreaming) {
474
  handleStop?.();
475
  return;
476
  }
477
 
478
+ // ignore if using input method engine
479
+ if (event.nativeEvent.isComposing) {
480
+ return;
481
  }
482
+
483
+ handleSendMessage?.(event);
484
+ }
485
+ }}
486
+ value={input}
487
+ onChange={(event) => {
488
+ handleInputChange?.(event);
489
+ }}
490
+ onPaste={handlePaste}
491
+ style={{
492
+ minHeight: TEXTAREA_MIN_HEIGHT,
493
+ maxHeight: TEXTAREA_MAX_HEIGHT,
494
+ }}
495
+ placeholder="How can Bolt help you today?"
496
+ translate="no"
497
+ />
498
+ <ClientOnly>
499
+ {() => (
500
+ <SendButton
501
+ show={input.length > 0 || isStreaming || uploadedFiles.length > 0}
502
+ isStreaming={isStreaming}
503
+ disabled={!providerList || providerList.length === 0}
504
+ onClick={(event) => {
505
+ if (isStreaming) {
506
+ handleStop?.();
507
+ return;
508
+ }
509
+
510
+ if (input.length > 0 || uploadedFiles.length > 0) {
511
+ handleSendMessage?.(event);
512
+ }
513
+ }}
514
+ />
515
+ )}
516
+ </ClientOnly>
517
+ <div className="flex justify-between items-center text-sm p-4 pt-2">
518
+ <div className="flex gap-1 items-center">
519
+ <IconButton title="Upload file" className="transition-all" onClick={() => handleFileUpload()}>
520
+ <div className="i-ph:paperclip text-xl"></div>
521
+ </IconButton>
522
+ <IconButton
523
+ title="Enhance prompt"
524
+ disabled={input.length === 0 || enhancingPrompt}
525
+ className={classNames('transition-all', enhancingPrompt ? 'opacity-100' : '')}
526
+ onClick={() => {
527
+ enhancePrompt?.();
528
+ toast.success('Prompt enhanced!');
529
+ }}
530
+ >
531
+ {enhancingPrompt ? (
532
+ <div className="i-svg-spinners:90-ring-with-bg text-bolt-elements-loader-progress text-xl animate-spin"></div>
533
+ ) : (
534
+ <div className="i-bolt:stars text-xl"></div>
535
+ )}
536
+ </IconButton>
537
+
538
+ <SpeechRecognitionButton
539
+ isListening={isListening}
540
+ onStart={startListening}
541
+ onStop={stopListening}
542
+ disabled={isStreaming}
543
+ />
544
+ {chatStarted && <ClientOnly>{() => <ExportChatButton exportChat={exportChat} />}</ClientOnly>}
545
+ <IconButton
546
+ title="Model Settings"
547
+ className={classNames('transition-all flex items-center gap-1', {
548
+ 'bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent':
549
+ isModelSettingsCollapsed,
550
+ 'bg-bolt-elements-item-backgroundDefault text-bolt-elements-item-contentDefault':
551
+ !isModelSettingsCollapsed,
552
+ })}
553
+ onClick={() => setIsModelSettingsCollapsed(!isModelSettingsCollapsed)}
554
+ disabled={!providerList || providerList.length === 0}
555
+ >
556
+ <div className={`i-ph:caret-${isModelSettingsCollapsed ? 'right' : 'down'} text-lg`} />
557
+ {isModelSettingsCollapsed ? <span className="text-xs">{model}</span> : <span />}
558
+ </IconButton>
559
  </div>
560
+ {input.length > 3 ? (
561
+ <div className="text-xs text-bolt-elements-textTertiary">
562
+ Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd>{' '}
563
+ + <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd>{' '}
564
+ a new line
565
+ </div>
566
+ ) : null}
567
+ </div>
568
  </div>
569
  </div>
570
  </div>
app/components/chat/Chat.client.tsx CHANGED
@@ -35,6 +35,9 @@ export function Chat() {
35
 
36
  const { ready, initialMessages, storeMessageHistory, importChat, exportChat } = useChatHistory();
37
  const title = useStore(description);
 
 
 
38
 
39
  return (
40
  <>
@@ -114,6 +117,7 @@ export const ChatImpl = memo(
114
  const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
115
  const [searchParams, setSearchParams] = useSearchParams();
116
  const files = useStore(workbenchStore.files);
 
117
  const { activeProviders, promptId } = useSettings();
118
 
119
  const [model, setModel] = useState(() => {
@@ -408,6 +412,8 @@ export const ChatImpl = memo(
408
  setUploadedFiles={setUploadedFiles}
409
  imageDataList={imageDataList}
410
  setImageDataList={setImageDataList}
 
 
411
  />
412
  );
413
  },
 
35
 
36
  const { ready, initialMessages, storeMessageHistory, importChat, exportChat } = useChatHistory();
37
  const title = useStore(description);
38
+ useEffect(() => {
39
+ workbenchStore.setReloadedMessages(initialMessages.map((m) => m.id));
40
+ }, [initialMessages]);
41
 
42
  return (
43
  <>
 
117
  const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
118
  const [searchParams, setSearchParams] = useSearchParams();
119
  const files = useStore(workbenchStore.files);
120
+ const actionAlert = useStore(workbenchStore.alert);
121
  const { activeProviders, promptId } = useSettings();
122
 
123
  const [model, setModel] = useState(() => {
 
412
  setUploadedFiles={setUploadedFiles}
413
  imageDataList={imageDataList}
414
  setImageDataList={setImageDataList}
415
+ actionAlert={actionAlert}
416
+ clearAlert={() => workbenchStore.clearAlert()}
417
  />
418
  );
419
  },
app/components/chat/ChatAlert.tsx ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AnimatePresence, motion } from 'framer-motion';
2
+ import type { ActionAlert } from '~/types/actions';
3
+ import { classNames } from '~/utils/classNames';
4
+
5
+ interface Props {
6
+ alert: ActionAlert;
7
+ clearAlert: () => void;
8
+ postMessage: (message: string) => void;
9
+ }
10
+
11
+ export default function ChatAlert({ alert, clearAlert, postMessage }: Props) {
12
+ const { description, content } = alert;
13
+
14
+ return (
15
+ <AnimatePresence>
16
+ <motion.div
17
+ initial={{ opacity: 0, y: -20 }}
18
+ animate={{ opacity: 1, y: 0 }}
19
+ exit={{ opacity: 0, y: -20 }}
20
+ transition={{ duration: 0.3 }}
21
+ className={`rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2 p-4`}
22
+ >
23
+ <div className="flex items-start">
24
+ {/* Icon */}
25
+ <motion.div
26
+ className="flex-shrink-0"
27
+ initial={{ scale: 0 }}
28
+ animate={{ scale: 1 }}
29
+ transition={{ delay: 0.2 }}
30
+ >
31
+ <div className={`i-ph:warning-duotone text-xl text-bolt-elements-button-danger-text`}></div>
32
+ </motion.div>
33
+ {/* Content */}
34
+ <div className="ml-3 flex-1">
35
+ <motion.h3
36
+ initial={{ opacity: 0 }}
37
+ animate={{ opacity: 1 }}
38
+ transition={{ delay: 0.1 }}
39
+ className={`text-sm font-medium text-bolt-elements-textPrimary`}
40
+ >
41
+ {/* {title} */}
42
+ Opps There is an error
43
+ </motion.h3>
44
+ <motion.div
45
+ initial={{ opacity: 0 }}
46
+ animate={{ opacity: 1 }}
47
+ transition={{ delay: 0.2 }}
48
+ className={`mt-2 text-sm text-bolt-elements-textSecondary`}
49
+ >
50
+ <p>
51
+ We encountered an error while running terminal commands. Would you like Bolt to analyze and help resolve
52
+ this issue?
53
+ </p>
54
+ {description && (
55
+ <div className="text-xs text-bolt-elements-textSecondary p-2 bg-bolt-elements-background-depth-3 rounded mt-4 mb-4">
56
+ Error: {description}
57
+ </div>
58
+ )}
59
+ </motion.div>
60
+
61
+ {/* Actions */}
62
+ <motion.div
63
+ className="mt-4"
64
+ initial={{ opacity: 0, y: 10 }}
65
+ animate={{ opacity: 1, y: 0 }}
66
+ transition={{ delay: 0.3 }}
67
+ >
68
+ <div className={classNames(' flex gap-2')}>
69
+ <button
70
+ onClick={() => postMessage(`*Fix this error on terminal* \n\`\`\`sh\n${content}\n\`\`\`\n`)}
71
+ className={classNames(
72
+ `px-2 py-1.5 rounded-md text-sm font-medium`,
73
+ 'bg-bolt-elements-button-primary-background',
74
+ 'hover:bg-bolt-elements-button-primary-backgroundHover',
75
+ 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-danger-background',
76
+ 'text-bolt-elements-button-primary-text',
77
+ 'flex items-center gap-1.5',
78
+ )}
79
+ >
80
+ <div className="i-ph:chat-circle-duotone"></div>
81
+ Ask Bolt
82
+ </button>
83
+ <button
84
+ onClick={clearAlert}
85
+ className={classNames(
86
+ `px-2 py-1.5 rounded-md text-sm font-medium`,
87
+ 'bg-bolt-elements-button-secondary-background',
88
+ 'hover:bg-bolt-elements-button-secondary-backgroundHover',
89
+ 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-secondary-background',
90
+ 'text-bolt-elements-button-secondary-text',
91
+ )}
92
+ >
93
+ Dismiss
94
+ </button>
95
+ </div>
96
+ </motion.div>
97
+ </div>
98
+ </div>
99
+ </motion.div>
100
+ </AnimatePresence>
101
+ );
102
+ }
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;
@@ -38,11 +39,15 @@ export class WorkbenchStore {
38
  #editorStore = new EditorStore(this.#filesStore);
39
  #terminalStore = new TerminalStore(webcontainer);
40
 
 
 
41
  artifacts: Artifacts = import.meta.hot?.data.artifacts ?? map({});
42
 
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 +57,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 +95,12 @@ export class WorkbenchStore {
89
  get boltTerminal() {
90
  return this.#terminalStore.boltTerminal;
91
  }
 
 
 
 
 
 
92
 
93
  toggleTerminal(value?: boolean) {
94
  this.#terminalStore.toggleTerminal(value);
@@ -233,6 +245,10 @@ export class WorkbenchStore {
233
  // TODO: what do we wanna do and how do we wanna recover from this?
234
  }
235
 
 
 
 
 
236
  addArtifact({ messageId, title, id, type }: ArtifactCallbackData) {
237
  const artifact = this.#getArtifact(messageId);
238
 
@@ -249,7 +265,17 @@ 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;
 
39
  #editorStore = new EditorStore(this.#filesStore);
40
  #terminalStore = new TerminalStore(webcontainer);
41
 
42
+ #reloadedMessages = new Set<string>();
43
+
44
  artifacts: Artifacts = import.meta.hot?.data.artifacts ?? map({});
45
 
46
  showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false);
47
  currentView: WritableAtom<WorkbenchViewType> = import.meta.hot?.data.currentView ?? atom('code');
48
  unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
49
+ actionAlert: WritableAtom<ActionAlert | undefined> =
50
+ import.meta.hot?.data.unsavedFiles ?? atom<ActionAlert | undefined>(undefined);
51
  modifiedFiles = new Set<string>();
52
  artifactIdList: string[] = [];
53
  #globalExecutionQueue = Promise.resolve();
 
57
  import.meta.hot.data.unsavedFiles = this.unsavedFiles;
58
  import.meta.hot.data.showWorkbench = this.showWorkbench;
59
  import.meta.hot.data.currentView = this.currentView;
60
+ import.meta.hot.data.actionAlert = this.actionAlert;
61
  }
62
  }
63
 
 
95
  get boltTerminal() {
96
  return this.#terminalStore.boltTerminal;
97
  }
98
+ get alert() {
99
+ return this.actionAlert;
100
+ }
101
+ clearAlert() {
102
+ this.actionAlert.set(undefined);
103
+ }
104
 
105
  toggleTerminal(value?: boolean) {
106
  this.#terminalStore.toggleTerminal(value);
 
245
  // TODO: what do we wanna do and how do we wanna recover from this?
246
  }
247
 
248
+ setReloadedMessages(messages: string[]) {
249
+ this.#reloadedMessages = new Set(messages);
250
+ }
251
+
252
  addArtifact({ messageId, title, id, type }: ArtifactCallbackData) {
253
  const artifact = this.#getArtifact(messageId);
254
 
 
265
  title,
266
  closed: false,
267
  type,
268
+ runner: new ActionRunner(
269
+ webcontainer,
270
+ () => this.boltTerminal,
271
+ (alert) => {
272
+ if (this.#reloadedMessages.has(messageId)) {
273
+ return;
274
+ }
275
+
276
+ this.actionAlert.set(alert);
277
+ },
278
+ ),
279
  });
280
  }
281
 
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: string;
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');
@@ -116,11 +122,19 @@ export class BoltShell {
116
 
117
  //wait for the execution to finish
118
  const executionPromise = this.getCurrentExecutionResult();
119
- this.executionState.set({ sessionId, active: true, executionPrms: executionPromise });
120
 
121
  const resp = await executionPromise;
122
  this.executionState.set({ sessionId, active: false });
123
 
 
 
 
 
 
 
 
 
124
  return resp;
125
  }
126
 
@@ -216,6 +230,65 @@ export class BoltShell {
216
  }
217
  }
218
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
219
  export function newBoltShellProcess() {
220
  return new BoltShell();
221
  }
 
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');
 
122
 
123
  //wait for the execution to finish
124
  const executionPromise = this.getCurrentExecutionResult();
125
+ this.executionState.set({ sessionId, active: true, executionPrms: executionPromise, abort });
126
 
127
  const resp = await executionPromise;
128
  this.executionState.set({ sessionId, active: false });
129
 
130
+ if (resp) {
131
+ try {
132
+ resp.output = cleanTerminalOutput(resp.output);
133
+ } catch (error) {
134
+ console.log('failed to format terminal output', error);
135
+ }
136
+ }
137
+
138
  return resp;
139
  }
140
 
 
230
  }
231
  }
232
 
233
+ /**
234
+ * Cleans and formats terminal output while preserving structure and paths
235
+ * Handles ANSI, OSC, and various terminal control sequences
236
+ */
237
+ export function cleanTerminalOutput(input: string): string {
238
+ // Step 1: Remove OSC sequences (including those with parameters)
239
+ const removeOsc = input
240
+ .replace(/\x1b\](\d+;[^\x07\x1b]*|\d+[^\x07\x1b]*)\x07/g, '')
241
+ .replace(/\](\d+;[^\n]*|\d+[^\n]*)/g, '');
242
+
243
+ // Step 2: Remove ANSI escape sequences and color codes more thoroughly
244
+ const removeAnsi = removeOsc
245
+ // Remove all escape sequences with parameters
246
+ .replace(/\u001b\[[\?]?[0-9;]*[a-zA-Z]/g, '')
247
+ .replace(/\x1b\[[\?]?[0-9;]*[a-zA-Z]/g, '')
248
+ // Remove color codes
249
+ .replace(/\u001b\[[0-9;]*m/g, '')
250
+ .replace(/\x1b\[[0-9;]*m/g, '')
251
+ // Clean up any remaining escape characters
252
+ .replace(/\u001b/g, '')
253
+ .replace(/\x1b/g, '');
254
+
255
+ // Step 3: Clean up carriage returns and newlines
256
+ const cleanNewlines = removeAnsi
257
+ .replace(/\r\n/g, '\n')
258
+ .replace(/\r/g, '\n')
259
+ .replace(/\n{3,}/g, '\n\n');
260
+
261
+ // Step 4: Add newlines at key breakpoints while preserving paths
262
+ const formatOutput = cleanNewlines
263
+ // Preserve prompt line
264
+ .replace(/^([~\/][^\n❯]+)❯/m, '$1\n❯')
265
+ // Add newline before command output indicators
266
+ .replace(/(?<!^|\n)>/g, '\n>')
267
+ // Add newline before error keywords without breaking paths
268
+ .replace(/(?<!^|\n|\w)(error|failed|warning|Error|Failed|Warning):/g, '\n$1:')
269
+ // Add newline before 'at' in stack traces without breaking paths
270
+ .replace(/(?<!^|\n|\/)(at\s+(?!async|sync))/g, '\nat ')
271
+ // Ensure 'at async' stays on same line
272
+ .replace(/\bat\s+async/g, 'at async')
273
+ // Add newline before npm error indicators
274
+ .replace(/(?<!^|\n)(npm ERR!)/g, '\n$1');
275
+
276
+ // Step 5: Clean up whitespace while preserving intentional spacing
277
+ const cleanSpaces = formatOutput
278
+ .split('\n')
279
+ .map((line) => line.trim())
280
+ .filter((line) => line.length > 0)
281
+ .join('\n');
282
+
283
+ // Step 6: Final cleanup
284
+ return cleanSpaces
285
+ .replace(/\n{3,}/g, '\n\n') // Replace multiple newlines with double newlines
286
+ .replace(/:\s+/g, ': ') // Normalize spacing after colons
287
+ .replace(/\s{2,}/g, ' ') // Remove multiple spaces
288
+ .replace(/^\s+|\s+$/g, '') // Trim start and end
289
+ .replace(/\u0000/g, ''); // Remove null characters
290
+ }
291
+
292
  export function newBoltShellProcess() {
293
  return new BoltShell();
294
  }