Merge pull request #797 from thecodacus/terminal-error-detection
Some checks are pending
Update Stable Branch / prepare-release (push) Waiting to run

feat: enhanced Terminal Error Handling and Alert System
This commit is contained in:
Eduard Ruzga 2024-12-23 14:25:39 +02:00 committed by GitHub
commit 79e7e75228
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 528 additions and 212 deletions

View File

@ -61,6 +61,7 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed
- ✅ PromptLibrary to have different variations of prompts for different use cases (@thecodacus) - ✅ PromptLibrary to have different variations of prompts for different use cases (@thecodacus)
- ✅ Detect package.json and commands to auto install & run preview for folder and git import (@wonderwhy-er) - ✅ Detect package.json and commands to auto install & run preview for folder and git import (@wonderwhy-er)
- ✅ Selection tool to target changes visually (@emcconnell) - ✅ Selection tool to target changes visually (@emcconnell)
- ✅ Detect terminal Errors and ask bolt to fix it (@thecodacus)
- ⬜ **HIGH PRIORITY** - Prevent bolt from rewriting files as often (file locking and diffs) - ⬜ **HIGH PRIORITY** - Prevent bolt from rewriting files as often (file locking and diffs)
- ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start) - ⬜ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
- ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call - ⬜ **HIGH PRIORITY** - Run agents in the backend as opposed to a single model call

View File

@ -28,6 +28,8 @@ import { SpeechRecognitionButton } from '~/components/chat/SpeechRecognition';
import type { IProviderSetting, ProviderInfo } from '~/types/model'; import type { IProviderSetting, ProviderInfo } from '~/types/model';
import { ScreenshotStateManager } from './ScreenshotStateManager'; import { ScreenshotStateManager } from './ScreenshotStateManager';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import type { ActionAlert } from '~/types/actions';
import ChatAlert from './ChatAlert';
const TEXTAREA_MIN_HEIGHT = 76; const TEXTAREA_MIN_HEIGHT = 76;
@ -58,6 +60,8 @@ interface BaseChatProps {
setUploadedFiles?: (files: File[]) => void; setUploadedFiles?: (files: File[]) => void;
imageDataList?: string[]; imageDataList?: string[];
setImageDataList?: (dataList: string[]) => void; setImageDataList?: (dataList: string[]) => void;
actionAlert?: ActionAlert;
clearAlert?: () => void;
} }
export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>( export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
@ -89,6 +93,8 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
imageDataList = [], imageDataList = [],
setImageDataList, setImageDataList,
messages, messages,
actionAlert,
clearAlert,
}, },
ref, ref,
) => { ) => {
@ -318,11 +324,31 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
}} }}
</ClientOnly> </ClientOnly>
<div <div
className={classNames( className={classNames('flex flex-col gap-4 w-full max-w-chat mx-auto z-prompt mb-6', {
'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',
{
'sticky bottom-2': chatStarted, 'sticky bottom-2': chatStarted,
}, })}
>
<div className="bg-bolt-elements-background-depth-2">
{actionAlert && (
<ChatAlert
alert={actionAlert}
clearAlert={() => clearAlert?.()}
postMessage={(message) => {
sendMessage?.({} as any, message);
clearAlert?.();
}}
/>
)}
</div>
<div
className={classNames(
'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',
/*
* {
* 'sticky bottom-2': chatStarted,
* },
*/
)} )}
> >
<svg className={classNames(styles.PromptEffectContainer)}> <svg className={classNames(styles.PromptEffectContainer)}>
@ -533,15 +559,16 @@ export const BaseChat = React.forwardRef<HTMLDivElement, BaseChatProps>(
</div> </div>
{input.length > 3 ? ( {input.length > 3 ? (
<div className="text-xs text-bolt-elements-textTertiary"> <div className="text-xs text-bolt-elements-textTertiary">
Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd> +{' '} Use <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Shift</kbd>{' '}
<kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd> a + <kbd className="kdb px-1.5 py-0.5 rounded bg-bolt-elements-background-depth-2">Return</kbd>{' '}
new line a new line
</div> </div>
) : null} ) : null}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div>
{!chatStarted && ( {!chatStarted && (
<div className="flex justify-center gap-2"> <div className="flex justify-center gap-2">
{ImportButtons(importChat)} {ImportButtons(importChat)}

View File

@ -35,6 +35,9 @@ export function Chat() {
const { ready, initialMessages, storeMessageHistory, importChat, exportChat } = useChatHistory(); const { ready, initialMessages, storeMessageHistory, importChat, exportChat } = useChatHistory();
const title = useStore(description); const title = useStore(description);
useEffect(() => {
workbenchStore.setReloadedMessages(initialMessages.map((m) => m.id));
}, [initialMessages]);
return ( return (
<> <>
@ -114,6 +117,7 @@ export const ChatImpl = memo(
const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here const [imageDataList, setImageDataList] = useState<string[]>([]); // Move here
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const files = useStore(workbenchStore.files); const files = useStore(workbenchStore.files);
const actionAlert = useStore(workbenchStore.alert);
const { activeProviders, promptId } = useSettings(); const { activeProviders, promptId } = useSettings();
const [model, setModel] = useState(() => { const [model, setModel] = useState(() => {
@ -408,6 +412,8 @@ export const ChatImpl = memo(
setUploadedFiles={setUploadedFiles} setUploadedFiles={setUploadedFiles}
imageDataList={imageDataList} imageDataList={imageDataList}
setImageDataList={setImageDataList} setImageDataList={setImageDataList}
actionAlert={actionAlert}
clearAlert={() => workbenchStore.clearAlert()}
/> />
); );
}, },

View File

@ -0,0 +1,102 @@
import { AnimatePresence, motion } from 'framer-motion';
import type { ActionAlert } from '~/types/actions';
import { classNames } from '~/utils/classNames';
interface Props {
alert: ActionAlert;
clearAlert: () => void;
postMessage: (message: string) => void;
}
export default function ChatAlert({ alert, clearAlert, postMessage }: Props) {
const { description, content } = alert;
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className={`rounded-lg border border-bolt-elements-borderColor bg-bolt-elements-background-depth-2 p-4`}
>
<div className="flex items-start">
{/* Icon */}
<motion.div
className="flex-shrink-0"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ delay: 0.2 }}
>
<div className={`i-ph:warning-duotone text-xl text-bolt-elements-button-danger-text`}></div>
</motion.div>
{/* Content */}
<div className="ml-3 flex-1">
<motion.h3
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1 }}
className={`text-sm font-medium text-bolt-elements-textPrimary`}
>
{/* {title} */}
Opps There is an error
</motion.h3>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.2 }}
className={`mt-2 text-sm text-bolt-elements-textSecondary`}
>
<p>
We encountered an error while running terminal commands. Would you like Bolt to analyze and help resolve
this issue?
</p>
{description && (
<div className="text-xs text-bolt-elements-textSecondary p-2 bg-bolt-elements-background-depth-3 rounded mt-4 mb-4">
Error: {description}
</div>
)}
</motion.div>
{/* Actions */}
<motion.div
className="mt-4"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<div className={classNames(' flex gap-2')}>
<button
onClick={() => postMessage(`*Fix this error on terminal* \n\`\`\`sh\n${content}\n\`\`\`\n`)}
className={classNames(
`px-2 py-1.5 rounded-md text-sm font-medium`,
'bg-bolt-elements-button-primary-background',
'hover:bg-bolt-elements-button-primary-backgroundHover',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-danger-background',
'text-bolt-elements-button-primary-text',
'flex items-center gap-1.5',
)}
>
<div className="i-ph:chat-circle-duotone"></div>
Ask Bolt
</button>
<button
onClick={clearAlert}
className={classNames(
`px-2 py-1.5 rounded-md text-sm font-medium`,
'bg-bolt-elements-button-secondary-background',
'hover:bg-bolt-elements-button-secondary-backgroundHover',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-bolt-elements-button-secondary-background',
'text-bolt-elements-button-secondary-text',
)}
>
Dismiss
</button>
</div>
</motion.div>
</div>
</div>
</motion.div>
</AnimatePresence>
);
}

View File

@ -1,7 +1,7 @@
import { WebContainer } from '@webcontainer/api'; import { WebContainer } from '@webcontainer/api';
import { atom, map, type MapStore } from 'nanostores'; import { atom, map, type MapStore } from 'nanostores';
import * as nodePath from 'node:path'; import * as nodePath from 'node:path';
import type { BoltAction } from '~/types/actions'; import type { ActionAlert, BoltAction } from '~/types/actions';
import { createScopedLogger } from '~/utils/logger'; import { createScopedLogger } from '~/utils/logger';
import { unreachable } from '~/utils/unreachable'; import { unreachable } from '~/utils/unreachable';
import type { ActionCallbackData } from './message-parser'; import type { ActionCallbackData } from './message-parser';
@ -34,16 +34,51 @@ export type ActionStateUpdate =
type ActionsMap = MapStore<Record<string, ActionState>>; type ActionsMap = MapStore<Record<string, ActionState>>;
class ActionCommandError extends Error {
readonly _output: string;
readonly _header: string;
constructor(message: string, output: string) {
// Create a formatted message that includes both the error message and output
const formattedMessage = `Failed To Execute Shell Command: ${message}\n\nOutput:\n${output}`;
super(formattedMessage);
// Set the output separately so it can be accessed programmatically
this._header = message;
this._output = output;
// Maintain proper prototype chain
Object.setPrototypeOf(this, ActionCommandError.prototype);
// Set the name of the error for better debugging
this.name = 'ActionCommandError';
}
// Optional: Add a method to get just the terminal output
get output() {
return this._output;
}
get header() {
return this._header;
}
}
export class ActionRunner { export class ActionRunner {
#webcontainer: Promise<WebContainer>; #webcontainer: Promise<WebContainer>;
#currentExecutionPromise: Promise<void> = Promise.resolve(); #currentExecutionPromise: Promise<void> = Promise.resolve();
#shellTerminal: () => BoltShell; #shellTerminal: () => BoltShell;
runnerId = atom<string>(`${Date.now()}`); runnerId = atom<string>(`${Date.now()}`);
actions: ActionsMap = map({}); actions: ActionsMap = map({});
onAlert?: (alert: ActionAlert) => void;
constructor(webcontainerPromise: Promise<WebContainer>, getShellTerminal: () => BoltShell) { constructor(
webcontainerPromise: Promise<WebContainer>,
getShellTerminal: () => BoltShell,
onAlert?: (alert: ActionAlert) => void,
) {
this.#webcontainer = webcontainerPromise; this.#webcontainer = webcontainerPromise;
this.#shellTerminal = getShellTerminal; this.#shellTerminal = getShellTerminal;
this.onAlert = onAlert;
} }
addAction(data: ActionCallbackData) { addAction(data: ActionCallbackData) {
@ -126,7 +161,25 @@ export class ActionRunner {
this.#runStartAction(action) this.#runStartAction(action)
.then(() => this.#updateAction(actionId, { status: 'complete' })) .then(() => this.#updateAction(actionId, { status: 'complete' }))
.catch(() => this.#updateAction(actionId, { status: 'failed', error: 'Action failed' })); .catch((err: Error) => {
if (action.abortSignal.aborted) {
return;
}
this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
logger.error(`[${action.type}]:Action failed\n\n`, err);
if (!(err instanceof ActionCommandError)) {
return;
}
this.onAlert?.({
type: 'error',
title: 'Dev Server Failed',
description: err.header,
content: err.output,
});
});
/* /*
* adding a delay to avoid any race condition between 2 start actions * adding a delay to avoid any race condition between 2 start actions
@ -142,9 +195,24 @@ export class ActionRunner {
status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete', status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete',
}); });
} catch (error) { } catch (error) {
if (action.abortSignal.aborted) {
return;
}
this.#updateAction(actionId, { status: 'failed', error: 'Action failed' }); this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
logger.error(`[${action.type}]:Action failed\n\n`, error); logger.error(`[${action.type}]:Action failed\n\n`, error);
if (!(error instanceof ActionCommandError)) {
return;
}
this.onAlert?.({
type: 'error',
title: 'Dev Server Failed',
description: error.header,
content: error.output,
});
// re-throw the error to be caught in the promise chain // re-throw the error to be caught in the promise chain
throw error; throw error;
} }
@ -162,11 +230,14 @@ export class ActionRunner {
unreachable('Shell terminal not found'); unreachable('Shell terminal not found');
} }
const resp = await shell.executeCommand(this.runnerId.get(), action.content); const resp = await shell.executeCommand(this.runnerId.get(), action.content, () => {
logger.debug(`[${action.type}]:Aborting Action\n\n`, action);
action.abort();
});
logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`); logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`);
if (resp?.exitCode != 0) { if (resp?.exitCode != 0) {
throw new Error('Failed To Execute Shell Command'); throw new ActionCommandError(`Failed To Execute Shell Command`, resp?.output || 'No Output Available');
} }
} }
@ -186,11 +257,14 @@ export class ActionRunner {
unreachable('Shell terminal not found'); unreachable('Shell terminal not found');
} }
const resp = await shell.executeCommand(this.runnerId.get(), action.content); const resp = await shell.executeCommand(this.runnerId.get(), action.content, () => {
logger.debug(`[${action.type}]:Aborting Action\n\n`, action);
action.abort();
});
logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`); logger.debug(`${action.type} Shell Response: [exit code:${resp?.exitCode}]`);
if (resp?.exitCode != 0) { if (resp?.exitCode != 0) {
throw new Error('Failed To Start Application'); throw new ActionCommandError('Failed To Start Application', resp?.output || 'No Output Available');
} }
return resp; return resp;

View File

@ -17,6 +17,7 @@ import { extractRelativePath } from '~/utils/diff';
import { description } from '~/lib/persistence'; import { description } from '~/lib/persistence';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { createSampler } from '~/utils/sampler'; import { createSampler } from '~/utils/sampler';
import type { ActionAlert } from '~/types/actions';
export interface ArtifactState { export interface ArtifactState {
id: string; id: string;
@ -38,11 +39,15 @@ export class WorkbenchStore {
#editorStore = new EditorStore(this.#filesStore); #editorStore = new EditorStore(this.#filesStore);
#terminalStore = new TerminalStore(webcontainer); #terminalStore = new TerminalStore(webcontainer);
#reloadedMessages = new Set<string>();
artifacts: Artifacts = import.meta.hot?.data.artifacts ?? map({}); artifacts: Artifacts = import.meta.hot?.data.artifacts ?? map({});
showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false); showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false);
currentView: WritableAtom<WorkbenchViewType> = import.meta.hot?.data.currentView ?? atom('code'); currentView: WritableAtom<WorkbenchViewType> = import.meta.hot?.data.currentView ?? atom('code');
unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>()); unsavedFiles: WritableAtom<Set<string>> = import.meta.hot?.data.unsavedFiles ?? atom(new Set<string>());
actionAlert: WritableAtom<ActionAlert | undefined> =
import.meta.hot?.data.unsavedFiles ?? atom<ActionAlert | undefined>(undefined);
modifiedFiles = new Set<string>(); modifiedFiles = new Set<string>();
artifactIdList: string[] = []; artifactIdList: string[] = [];
#globalExecutionQueue = Promise.resolve(); #globalExecutionQueue = Promise.resolve();
@ -52,6 +57,7 @@ export class WorkbenchStore {
import.meta.hot.data.unsavedFiles = this.unsavedFiles; import.meta.hot.data.unsavedFiles = this.unsavedFiles;
import.meta.hot.data.showWorkbench = this.showWorkbench; import.meta.hot.data.showWorkbench = this.showWorkbench;
import.meta.hot.data.currentView = this.currentView; import.meta.hot.data.currentView = this.currentView;
import.meta.hot.data.actionAlert = this.actionAlert;
} }
} }
@ -89,6 +95,12 @@ export class WorkbenchStore {
get boltTerminal() { get boltTerminal() {
return this.#terminalStore.boltTerminal; return this.#terminalStore.boltTerminal;
} }
get alert() {
return this.actionAlert;
}
clearAlert() {
this.actionAlert.set(undefined);
}
toggleTerminal(value?: boolean) { toggleTerminal(value?: boolean) {
this.#terminalStore.toggleTerminal(value); this.#terminalStore.toggleTerminal(value);
@ -233,6 +245,10 @@ export class WorkbenchStore {
// TODO: what do we wanna do and how do we wanna recover from this? // TODO: what do we wanna do and how do we wanna recover from this?
} }
setReloadedMessages(messages: string[]) {
this.#reloadedMessages = new Set(messages);
}
addArtifact({ messageId, title, id, type }: ArtifactCallbackData) { addArtifact({ messageId, title, id, type }: ArtifactCallbackData) {
const artifact = this.#getArtifact(messageId); const artifact = this.#getArtifact(messageId);
@ -249,7 +265,17 @@ export class WorkbenchStore {
title, title,
closed: false, closed: false,
type, type,
runner: new ActionRunner(webcontainer, () => this.boltTerminal), runner: new ActionRunner(
webcontainer,
() => this.boltTerminal,
(alert) => {
if (this.#reloadedMessages.has(messageId)) {
return;
}
this.actionAlert.set(alert);
},
),
}); });
} }

View File

@ -20,3 +20,10 @@ export interface StartAction extends BaseAction {
export type BoltAction = FileAction | ShellAction | StartAction; export type BoltAction = FileAction | ShellAction | StartAction;
export type BoltActionData = BoltAction | BaseAction; export type BoltActionData = BoltAction | BaseAction;
export interface ActionAlert {
type: string;
title: string;
description: string;
content: string;
}

View File

@ -60,7 +60,9 @@ export class BoltShell {
#webcontainer: WebContainer | undefined; #webcontainer: WebContainer | undefined;
#terminal: ITerminal | undefined; #terminal: ITerminal | undefined;
#process: WebContainerProcess | undefined; #process: WebContainerProcess | undefined;
executionState = atom<{ sessionId: string; active: boolean; executionPrms?: Promise<any> } | undefined>(); executionState = atom<
{ sessionId: string; active: boolean; executionPrms?: Promise<any>; abort?: () => void } | undefined
>();
#outputStream: ReadableStreamDefaultReader<string> | undefined; #outputStream: ReadableStreamDefaultReader<string> | undefined;
#shellInputStream: WritableStreamDefaultWriter<string> | undefined; #shellInputStream: WritableStreamDefaultWriter<string> | undefined;
@ -93,13 +95,17 @@ export class BoltShell {
return this.#process; return this.#process;
} }
async executeCommand(sessionId: string, command: string): Promise<ExecutionResult> { async executeCommand(sessionId: string, command: string, abort?: () => void): Promise<ExecutionResult> {
if (!this.process || !this.terminal) { if (!this.process || !this.terminal) {
return undefined; return undefined;
} }
const state = this.executionState.get(); const state = this.executionState.get();
if (state?.active && state.abort) {
state.abort();
}
/* /*
* interrupt the current execution * interrupt the current execution
* this.#shellInputStream?.write('\x03'); * this.#shellInputStream?.write('\x03');
@ -116,11 +122,19 @@ export class BoltShell {
//wait for the execution to finish //wait for the execution to finish
const executionPromise = this.getCurrentExecutionResult(); const executionPromise = this.getCurrentExecutionResult();
this.executionState.set({ sessionId, active: true, executionPrms: executionPromise }); this.executionState.set({ sessionId, active: true, executionPrms: executionPromise, abort });
const resp = await executionPromise; const resp = await executionPromise;
this.executionState.set({ sessionId, active: false }); this.executionState.set({ sessionId, active: false });
if (resp) {
try {
resp.output = cleanTerminalOutput(resp.output);
} catch (error) {
console.log('failed to format terminal output', error);
}
}
return resp; return resp;
} }
@ -216,6 +230,65 @@ export class BoltShell {
} }
} }
/**
* Cleans and formats terminal output while preserving structure and paths
* Handles ANSI, OSC, and various terminal control sequences
*/
export function cleanTerminalOutput(input: string): string {
// Step 1: Remove OSC sequences (including those with parameters)
const removeOsc = input
.replace(/\x1b\](\d+;[^\x07\x1b]*|\d+[^\x07\x1b]*)\x07/g, '')
.replace(/\](\d+;[^\n]*|\d+[^\n]*)/g, '');
// Step 2: Remove ANSI escape sequences and color codes more thoroughly
const removeAnsi = removeOsc
// Remove all escape sequences with parameters
.replace(/\u001b\[[\?]?[0-9;]*[a-zA-Z]/g, '')
.replace(/\x1b\[[\?]?[0-9;]*[a-zA-Z]/g, '')
// Remove color codes
.replace(/\u001b\[[0-9;]*m/g, '')
.replace(/\x1b\[[0-9;]*m/g, '')
// Clean up any remaining escape characters
.replace(/\u001b/g, '')
.replace(/\x1b/g, '');
// Step 3: Clean up carriage returns and newlines
const cleanNewlines = removeAnsi
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.replace(/\n{3,}/g, '\n\n');
// Step 4: Add newlines at key breakpoints while preserving paths
const formatOutput = cleanNewlines
// Preserve prompt line
.replace(/^([~\/][^\n]+)/m, '$1\n')
// Add newline before command output indicators
.replace(/(?<!^|\n)>/g, '\n>')
// Add newline before error keywords without breaking paths
.replace(/(?<!^|\n|\w)(error|failed|warning|Error|Failed|Warning):/g, '\n$1:')
// Add newline before 'at' in stack traces without breaking paths
.replace(/(?<!^|\n|\/)(at\s+(?!async|sync))/g, '\nat ')
// Ensure 'at async' stays on same line
.replace(/\bat\s+async/g, 'at async')
// Add newline before npm error indicators
.replace(/(?<!^|\n)(npm ERR!)/g, '\n$1');
// Step 5: Clean up whitespace while preserving intentional spacing
const cleanSpaces = formatOutput
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length > 0)
.join('\n');
// Step 6: Final cleanup
return cleanSpaces
.replace(/\n{3,}/g, '\n\n') // Replace multiple newlines with double newlines
.replace(/:\s+/g, ': ') // Normalize spacing after colons
.replace(/\s{2,}/g, ' ') // Remove multiple spaces
.replace(/^\s+|\s+$/g, '') // Trim start and end
.replace(/\u0000/g, ''); // Remove null characters
}
export function newBoltShellProcess() { export function newBoltShellProcess() {
return new BoltShell(); return new BoltShell();
} }