Merge pull request #797 from thecodacus/terminal-error-detection
Browse files- README.md +1 -0
- app/components/chat/BaseChat.tsx +229 -202
- app/components/chat/Chat.client.tsx +6 -0
- app/components/chat/ChatAlert.tsx +102 -0
- app/lib/runtime/action-runner.ts +81 -7
- app/lib/stores/workbench.ts +27 -1
- app/types/actions.ts +7 -0
- app/utils/shell.ts +76 -3
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 |
-
'
|
323 |
-
|
324 |
-
'sticky bottom-2': chatStarted,
|
325 |
-
},
|
326 |
-
)}
|
327 |
>
|
328 |
-
<
|
329 |
-
|
330 |
-
<
|
331 |
-
|
332 |
-
|
333 |
-
|
334 |
-
|
335 |
-
|
336 |
-
|
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 |
-
</
|
397 |
<div
|
398 |
className={classNames(
|
399 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
400 |
)}
|
401 |
>
|
402 |
-
<
|
403 |
-
|
404 |
-
|
405 |
-
|
406 |
-
|
407 |
-
|
408 |
-
|
409 |
-
|
410 |
-
|
411 |
-
|
412 |
-
|
413 |
-
|
414 |
-
|
415 |
-
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
425 |
-
|
426 |
-
|
427 |
-
|
428 |
-
|
429 |
-
|
430 |
-
|
431 |
-
|
432 |
-
|
433 |
-
|
434 |
-
|
435 |
-
|
436 |
-
}
|
437 |
-
|
438 |
-
|
439 |
-
|
440 |
-
|
441 |
-
|
442 |
-
|
443 |
-
|
444 |
-
|
445 |
-
|
446 |
-
|
447 |
-
|
448 |
-
|
449 |
-
|
450 |
-
|
451 |
-
|
452 |
-
|
453 |
-
|
454 |
-
|
455 |
-
|
456 |
-
|
457 |
-
|
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 |
-
<
|
475 |
-
|
476 |
-
|
477 |
-
|
478 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
479 |
if (isStreaming) {
|
480 |
handleStop?.();
|
481 |
return;
|
482 |
}
|
483 |
|
484 |
-
|
485 |
-
|
|
|
486 |
}
|
487 |
-
|
488 |
-
|
489 |
-
|
490 |
-
|
491 |
-
|
492 |
-
|
493 |
-
|
494 |
-
|
495 |
-
|
496 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
505 |
-
|
506 |
-
|
507 |
-
|
508 |
-
|
509 |
-
|
510 |
-
|
511 |
-
|
512 |
-
|
513 |
-
|
514 |
-
|
515 |
-
|
516 |
-
|
517 |
-
|
518 |
-
|
519 |
-
|
520 |
-
|
521 |
-
|
522 |
-
|
523 |
-
|
524 |
-
|
525 |
-
|
526 |
-
|
527 |
-
|
528 |
-
|
529 |
-
|
530 |
-
|
531 |
-
|
532 |
-
|
533 |
-
|
534 |
-
|
535 |
-
|
536 |
-
|
537 |
-
|
538 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
539 |
</div>
|
540 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(
|
|
|
|
|
|
|
|
|
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(() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
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
|
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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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<
|
|
|
|
|
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 |
}
|