feat: catch errors from web container preview and show in actionable alert so user can send them to AI for fixing (#856)
Browse files* Catch errors from web container
* Show fix error popup on errors in preview
* Remove unneeded action type
* PR comments
* Cleanup urls in stacktrace
---------
Co-authored-by: Anirban Kar <thecodacus@gmail.com>
- README.md +1 -0
- app/components/chat/ChatAlert.tsx +14 -8
- app/lib/webcontainer/index.ts +26 -2
- app/types/actions.ts +1 -0
- app/utils/stacktrace.ts +27 -0
README.md
CHANGED
|
@@ -62,6 +62,7 @@ bolt.diy was originally started by [Cole Medin](https://www.youtube.com/@ColeMed
|
|
| 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 |
- β
Add Starter Template Options (@thecodacus)
|
| 66 |
- β¬ **HIGH PRIORITY** - Prevent bolt from rewriting files as often (file locking and diffs)
|
| 67 |
- β¬ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
|
|
|
|
| 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 |
+
- β
Detect preview Errors and ask bolt to fix it (@wonderwhy-er)
|
| 66 |
- β
Add Starter Template Options (@thecodacus)
|
| 67 |
- β¬ **HIGH PRIORITY** - Prevent bolt from rewriting files as often (file locking and diffs)
|
| 68 |
- β¬ **HIGH PRIORITY** - Better prompting for smaller LLMs (code window sometimes doesn't start)
|
app/components/chat/ChatAlert.tsx
CHANGED
|
@@ -9,7 +9,13 @@ interface Props {
|
|
| 9 |
}
|
| 10 |
|
| 11 |
export default function ChatAlert({ alert, clearAlert, postMessage }: Props) {
|
| 12 |
-
const { description, content } = alert;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
return (
|
| 15 |
<AnimatePresence>
|
|
@@ -38,8 +44,7 @@ export default function ChatAlert({ alert, clearAlert, postMessage }: Props) {
|
|
| 38 |
transition={{ delay: 0.1 }}
|
| 39 |
className={`text-sm font-medium text-bolt-elements-textPrimary`}
|
| 40 |
>
|
| 41 |
-
{
|
| 42 |
-
Opps There is an error
|
| 43 |
</motion.h3>
|
| 44 |
<motion.div
|
| 45 |
initial={{ opacity: 0 }}
|
|
@@ -47,10 +52,7 @@ export default function ChatAlert({ alert, clearAlert, postMessage }: Props) {
|
|
| 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}
|
|
@@ -67,7 +69,11 @@ export default function ChatAlert({ alert, clearAlert, postMessage }: Props) {
|
|
| 67 |
>
|
| 68 |
<div className={classNames(' flex gap-2')}>
|
| 69 |
<button
|
| 70 |
-
onClick={() =>
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
className={classNames(
|
| 72 |
`px-2 py-1.5 rounded-md text-sm font-medium`,
|
| 73 |
'bg-bolt-elements-button-primary-background',
|
|
|
|
| 9 |
}
|
| 10 |
|
| 11 |
export default function ChatAlert({ alert, clearAlert, postMessage }: Props) {
|
| 12 |
+
const { description, content, source } = alert;
|
| 13 |
+
|
| 14 |
+
const isPreview = source === 'preview';
|
| 15 |
+
const title = isPreview ? 'Preview Error' : 'Terminal Error';
|
| 16 |
+
const message = isPreview
|
| 17 |
+
? 'We encountered an error while running the preview. Would you like Bolt to analyze and help resolve this issue?'
|
| 18 |
+
: 'We encountered an error while running terminal commands. Would you like Bolt to analyze and help resolve this issue?';
|
| 19 |
|
| 20 |
return (
|
| 21 |
<AnimatePresence>
|
|
|
|
| 44 |
transition={{ delay: 0.1 }}
|
| 45 |
className={`text-sm font-medium text-bolt-elements-textPrimary`}
|
| 46 |
>
|
| 47 |
+
{title}
|
|
|
|
| 48 |
</motion.h3>
|
| 49 |
<motion.div
|
| 50 |
initial={{ opacity: 0 }}
|
|
|
|
| 52 |
transition={{ delay: 0.2 }}
|
| 53 |
className={`mt-2 text-sm text-bolt-elements-textSecondary`}
|
| 54 |
>
|
| 55 |
+
<p>{message}</p>
|
|
|
|
|
|
|
|
|
|
| 56 |
{description && (
|
| 57 |
<div className="text-xs text-bolt-elements-textSecondary p-2 bg-bolt-elements-background-depth-3 rounded mt-4 mb-4">
|
| 58 |
Error: {description}
|
|
|
|
| 69 |
>
|
| 70 |
<div className={classNames(' flex gap-2')}>
|
| 71 |
<button
|
| 72 |
+
onClick={() =>
|
| 73 |
+
postMessage(
|
| 74 |
+
`*Fix this ${isPreview ? 'preview' : 'terminal'} error* \n\`\`\`${isPreview ? 'js' : 'sh'}\n${content}\n\`\`\`\n`,
|
| 75 |
+
)
|
| 76 |
+
}
|
| 77 |
className={classNames(
|
| 78 |
`px-2 py-1.5 rounded-md text-sm font-medium`,
|
| 79 |
'bg-bolt-elements-button-primary-background',
|
app/lib/webcontainer/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
| 1 |
import { WebContainer } from '@webcontainer/api';
|
| 2 |
import { WORK_DIR_NAME } from '~/utils/constants';
|
|
|
|
| 3 |
|
| 4 |
interface WebContainerContext {
|
| 5 |
loaded: boolean;
|
|
@@ -22,10 +23,33 @@ if (!import.meta.env.SSR) {
|
|
| 22 |
import.meta.hot?.data.webcontainer ??
|
| 23 |
Promise.resolve()
|
| 24 |
.then(() => {
|
| 25 |
-
return WebContainer.boot({
|
|
|
|
|
|
|
|
|
|
| 26 |
})
|
| 27 |
-
.then((webcontainer) => {
|
| 28 |
webcontainerContext.loaded = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
return webcontainer;
|
| 30 |
});
|
| 31 |
|
|
|
|
| 1 |
import { WebContainer } from '@webcontainer/api';
|
| 2 |
import { WORK_DIR_NAME } from '~/utils/constants';
|
| 3 |
+
import { cleanStackTrace } from '~/utils/stacktrace';
|
| 4 |
|
| 5 |
interface WebContainerContext {
|
| 6 |
loaded: boolean;
|
|
|
|
| 23 |
import.meta.hot?.data.webcontainer ??
|
| 24 |
Promise.resolve()
|
| 25 |
.then(() => {
|
| 26 |
+
return WebContainer.boot({
|
| 27 |
+
workdirName: WORK_DIR_NAME,
|
| 28 |
+
forwardPreviewErrors: true, // Enable error forwarding from iframes
|
| 29 |
+
});
|
| 30 |
})
|
| 31 |
+
.then(async (webcontainer) => {
|
| 32 |
webcontainerContext.loaded = true;
|
| 33 |
+
|
| 34 |
+
const { workbenchStore } = await import('~/lib/stores/workbench');
|
| 35 |
+
|
| 36 |
+
// Listen for preview errors
|
| 37 |
+
webcontainer.on('preview-message', (message) => {
|
| 38 |
+
console.log('WebContainer preview message:', message);
|
| 39 |
+
|
| 40 |
+
// Handle both uncaught exceptions and unhandled promise rejections
|
| 41 |
+
if (message.type === 'PREVIEW_UNCAUGHT_EXCEPTION' || message.type === 'PREVIEW_UNHANDLED_REJECTION') {
|
| 42 |
+
const isPromise = message.type === 'PREVIEW_UNHANDLED_REJECTION';
|
| 43 |
+
workbenchStore.actionAlert.set({
|
| 44 |
+
type: 'preview',
|
| 45 |
+
title: isPromise ? 'Unhandled Promise Rejection' : 'Uncaught Exception',
|
| 46 |
+
description: message.message,
|
| 47 |
+
content: `Error occurred at ${message.pathname}${message.search}${message.hash}\nPort: ${message.port}\n\nStack trace:\n${cleanStackTrace(message.stack || '')}`,
|
| 48 |
+
source: 'preview',
|
| 49 |
+
});
|
| 50 |
+
}
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
return webcontainer;
|
| 54 |
});
|
| 55 |
|
app/types/actions.ts
CHANGED
|
@@ -26,4 +26,5 @@ export interface ActionAlert {
|
|
| 26 |
title: string;
|
| 27 |
description: string;
|
| 28 |
content: string;
|
|
|
|
| 29 |
}
|
|
|
|
| 26 |
title: string;
|
| 27 |
description: string;
|
| 28 |
content: string;
|
| 29 |
+
source?: 'terminal' | 'preview'; // Add source to differentiate between terminal and preview errors
|
| 30 |
}
|
app/utils/stacktrace.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Cleans webcontainer URLs from stack traces to show relative paths instead
|
| 3 |
+
*/
|
| 4 |
+
export function cleanStackTrace(stackTrace: string): string {
|
| 5 |
+
// Function to clean a single URL
|
| 6 |
+
const cleanUrl = (url: string): string => {
|
| 7 |
+
const regex = /^https?:\/\/[^\/]+\.webcontainer-api\.io(\/.*)?$/;
|
| 8 |
+
|
| 9 |
+
if (!regex.test(url)) {
|
| 10 |
+
return url;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const pathRegex = /^https?:\/\/[^\/]+\.webcontainer-api\.io\/(.*?)$/;
|
| 14 |
+
const match = url.match(pathRegex);
|
| 15 |
+
|
| 16 |
+
return match?.[1] || '';
|
| 17 |
+
};
|
| 18 |
+
|
| 19 |
+
// Split the stack trace into lines and process each line
|
| 20 |
+
return stackTrace
|
| 21 |
+
.split('\n')
|
| 22 |
+
.map((line) => {
|
| 23 |
+
// Match any URL in the line that contains webcontainer-api.io
|
| 24 |
+
return line.replace(/(https?:\/\/[^\/]+\.webcontainer-api\.io\/[^\s\)]+)/g, (match) => cleanUrl(match));
|
| 25 |
+
})
|
| 26 |
+
.join('\n');
|
| 27 |
+
}
|