feat: added terminal error capturing and automated fix prompt
Browse files- app/commit.json +1 -1
- app/components/chat/BaseChat.tsx +229 -202
- app/components/chat/Chat.client.tsx +3 -0
- app/components/chat/ChatAlert.tsx +81 -0
- app/lib/runtime/action-runner.ts +81 -7
- app/lib/stores/workbench.ts +15 -1
- app/types/actions.ts +7 -0
- app/utils/shell.ts +58 -3
app/commit.json
CHANGED
@@ -1 +1 @@
|
|
1 |
-
{ "commit": "
|
|
|
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 |
-
'
|
318 |
-
|
319 |
-
'sticky bottom-2': chatStarted,
|
320 |
-
},
|
321 |
-
)}
|
322 |
>
|
323 |
-
<
|
324 |
-
|
325 |
-
<
|
326 |
-
|
327 |
-
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
|
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 |
-
</
|
392 |
<div
|
393 |
className={classNames(
|
394 |
-
'
|
|
|
|
|
|
|
|
|
|
|
|
|
395 |
)}
|
396 |
>
|
397 |
-
<
|
398 |
-
|
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 |
-
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 |
-
<
|
470 |
-
|
471 |
-
|
472 |
-
|
473 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
474 |
if (isStreaming) {
|
475 |
handleStop?.();
|
476 |
return;
|
477 |
}
|
478 |
|
479 |
-
|
480 |
-
|
|
|
481 |
}
|
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 |
</div>
|
535 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(
|
|
|
|
|
|
|
|
|
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;
|
@@ -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(
|
|
|
|
|
|
|
|
|
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<
|
|
|
|
|
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 |
}
|