feat: added terminal error capturing and automated fix prompt

This commit is contained in:
Anirban Kar
2024-12-17 21:19:43 +05:30
parent 42bde1cae4
commit d327cfea29
8 changed files with 474 additions and 213 deletions

View File

@@ -1,7 +1,7 @@
import { WebContainer } from '@webcontainer/api';
import { atom, map, type MapStore } from 'nanostores';
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 { unreachable } from '~/utils/unreachable';
import type { ActionCallbackData } from './message-parser';
@@ -34,16 +34,51 @@ export type ActionStateUpdate =
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 {
#webcontainer: Promise<WebContainer>;
#currentExecutionPromise: Promise<void> = Promise.resolve();
#shellTerminal: () => BoltShell;
runnerId = atom<string>(`${Date.now()}`);
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.#shellTerminal = getShellTerminal;
this.onAlert = onAlert;
}
addAction(data: ActionCallbackData) {
@@ -126,7 +161,25 @@ export class ActionRunner {
this.#runStartAction(action)
.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
@@ -142,9 +195,24 @@ export class ActionRunner {
status: isStreaming ? 'running' : action.abortSignal.aborted ? 'aborted' : 'complete',
});
} catch (error) {
if (action.abortSignal.aborted) {
return;
}
this.#updateAction(actionId, { status: 'failed', error: 'Action failed' });
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
throw error;
}
@@ -162,11 +230,14 @@ export class ActionRunner {
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}]`);
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');
}
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}]`);
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;

View File

@@ -17,6 +17,7 @@ import { extractRelativePath } from '~/utils/diff';
import { description } from '~/lib/persistence';
import Cookies from 'js-cookie';
import { createSampler } from '~/utils/sampler';
import type { ActionAlert } from '~/types/actions';
export interface ArtifactState {
id: string;
@@ -43,6 +44,8 @@ export class WorkbenchStore {
showWorkbench: WritableAtom<boolean> = import.meta.hot?.data.showWorkbench ?? atom(false);
currentView: WritableAtom<WorkbenchViewType> = import.meta.hot?.data.currentView ?? atom('code');
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>();
artifactIdList: string[] = [];
#globalExecutionQueue = Promise.resolve();
@@ -52,6 +55,7 @@ export class WorkbenchStore {
import.meta.hot.data.unsavedFiles = this.unsavedFiles;
import.meta.hot.data.showWorkbench = this.showWorkbench;
import.meta.hot.data.currentView = this.currentView;
import.meta.hot.data.actionAlert = this.actionAlert;
}
}
@@ -89,6 +93,12 @@ export class WorkbenchStore {
get boltTerminal() {
return this.#terminalStore.boltTerminal;
}
get alert() {
return this.actionAlert;
}
clearAlert() {
this.actionAlert.set(undefined);
}
toggleTerminal(value?: boolean) {
this.#terminalStore.toggleTerminal(value);
@@ -249,7 +259,11 @@ export class WorkbenchStore {
title,
closed: false,
type,
runner: new ActionRunner(webcontainer, () => this.boltTerminal),
runner: new ActionRunner(
webcontainer,
() => this.boltTerminal,
(alert) => this.actionAlert.set(alert),
),
});
}