Eduards codacus commited on
Commit
7eee038
Β·
unverified Β·
1 Parent(s): fc4f89f

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 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
- {/* {title} */}
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={() => postMessage(`*Fix this error on terminal* \n\`\`\`sh\n${content}\n\`\`\`\n`)}
 
 
 
 
71
  className={classNames(
72
  `px-2 py-1.5 rounded-md text-sm font-medium`,
73
  'bg-bolt-elements-button-primary-background',
 
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({ workdirName: WORK_DIR_NAME });
 
 
 
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
+ }