mirror of
https://github.com/coleam00/bolt.new-any-llm
synced 2024-12-28 06:42:56 +00:00
Merge pull request #797 from thecodacus/terminal-error-detection
Some checks are pending
Update Stable Branch / prepare-release (push) Waiting to run
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:
commit
79e7e75228
@ -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
|
||||||
|
@ -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)}
|
||||||
|
@ -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()}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
102
app/components/chat/ChatAlert.tsx
Normal file
102
app/components/chat/ChatAlert.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
},
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user