Better spawn error message (#1434)
<!-- CURSOR_SUMMARY -->
> [!NOTE]
> Structure preview errors with source-aware messaging/UI and enhance
local/Docker spawn error diagnostics and logging.
>
> - **Frontend**:
> - **Error model**: Change `previewErrorMessageAtom` from `string` to
`{ message, source }` to distinguish `preview-app` vs `dyad-app` errors.
> - **Preview UI**: Update `ErrorBanner` in
`components/preview_panel/PreviewIframe.tsx` to use `error.message`,
show an "Internal Dyad error" chip for `dyad-app`, conditional tip text,
and hide AI fix for non-`preview-app` errors; use `cn` helper.
> - **Error propagation**: Wrap iframe and build errors via
`setErrorMessage({ message, source: "preview-app" })`; adjust AI prompt
to use `errorMessage.message`.
> - **Hooks**:
> - `useRunApp`: On run/stop/restart failures, set `{ message, source:
"dyad-app" }` in `previewErrorMessageAtom`.
> - **Backend**:
> - `ipc/handlers/app_handlers.ts`: Improve spawn failure handling for
local node and Docker: capture stderr as strings, collect error details
(`message`, `code`, `errno`, `syscall`, `path`, `spawnargs`), log with
context, and throw enriched error messages.
>
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
4135b04e19431dd53848c3266e5211e4c9df6aa2. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
This commit is contained in:
@@ -23,4 +23,6 @@ export const envVarsAtom = atom<Record<string, string | undefined>>({});
|
|||||||
|
|
||||||
export const previewPanelKeyAtom = atom<number>(0);
|
export const previewPanelKeyAtom = atom<number>(0);
|
||||||
|
|
||||||
export const previewErrorMessageAtom = atom<string | undefined>(undefined);
|
export const previewErrorMessageAtom = atom<
|
||||||
|
{ message: string; source: "preview-app" | "dyad-app" } | undefined
|
||||||
|
>(undefined);
|
||||||
|
|||||||
@@ -41,9 +41,10 @@ import {
|
|||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { useRunApp } from "@/hooks/useRunApp";
|
import { useRunApp } from "@/hooks/useRunApp";
|
||||||
import { useShortcut } from "@/hooks/useShortcut";
|
import { useShortcut } from "@/hooks/useShortcut";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface ErrorBannerProps {
|
interface ErrorBannerProps {
|
||||||
error: string | undefined;
|
error: { message: string; source: "preview-app" | "dyad-app" } | undefined;
|
||||||
onDismiss: () => void;
|
onDismiss: () => void;
|
||||||
onAIFix: () => void;
|
onAIFix: () => void;
|
||||||
}
|
}
|
||||||
@@ -52,12 +53,12 @@ const ErrorBanner = ({ error, onDismiss, onAIFix }: ErrorBannerProps) => {
|
|||||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||||
const { isStreaming } = useStreamChat();
|
const { isStreaming } = useStreamChat();
|
||||||
if (!error) return null;
|
if (!error) return null;
|
||||||
const isDockerError = error.includes("Cannot connect to the Docker");
|
const isDockerError = error.message.includes("Cannot connect to the Docker");
|
||||||
|
|
||||||
const getTruncatedError = () => {
|
const getTruncatedError = () => {
|
||||||
const firstLine = error.split("\n")[0];
|
const firstLine = error.message.split("\n")[0];
|
||||||
const snippetLength = 250;
|
const snippetLength = 250;
|
||||||
const snippet = error.substring(0, snippetLength);
|
const snippet = error.message.substring(0, snippetLength);
|
||||||
return firstLine.length < snippet.length
|
return firstLine.length < snippet.length
|
||||||
? firstLine
|
? firstLine
|
||||||
: snippet + (snippet.length === snippetLength ? "..." : "");
|
: snippet + (snippet.length === snippetLength ? "..." : "");
|
||||||
@@ -76,8 +77,20 @@ const ErrorBanner = ({ error, onDismiss, onAIFix }: ErrorBannerProps) => {
|
|||||||
<X size={14} className="text-red-500 dark:text-red-400" />
|
<X size={14} className="text-red-500 dark:text-red-400" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Add a little chip that says "Internal error" if source is "dyad-app" */}
|
||||||
|
{error.source === "dyad-app" && (
|
||||||
|
<div className="absolute top-1 right-1 p-1 bg-red-100 dark:bg-red-900 rounded-md text-xs font-medium text-red-700 dark:text-red-300">
|
||||||
|
Internal Dyad error
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Error message in the middle */}
|
{/* Error message in the middle */}
|
||||||
<div className="px-6 py-1 text-sm">
|
<div
|
||||||
|
className={cn(
|
||||||
|
"px-6 py-1 text-sm",
|
||||||
|
error.source === "dyad-app" && "pt-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className="text-red-700 dark:text-red-300 text-wrap font-mono whitespace-pre-wrap break-words text-xs cursor-pointer flex gap-1 items-start"
|
className="text-red-700 dark:text-red-300 text-wrap font-mono whitespace-pre-wrap break-words text-xs cursor-pointer flex gap-1 items-start"
|
||||||
onClick={() => setIsCollapsed(!isCollapsed)}
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
@@ -88,7 +101,7 @@ const ErrorBanner = ({ error, onDismiss, onAIFix }: ErrorBannerProps) => {
|
|||||||
isCollapsed ? "" : "rotate-90"
|
isCollapsed ? "" : "rotate-90"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
{isCollapsed ? getTruncatedError() : error}
|
{isCollapsed ? getTruncatedError() : error.message}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -102,13 +115,15 @@ const ErrorBanner = ({ error, onDismiss, onAIFix }: ErrorBannerProps) => {
|
|||||||
<span className="font-medium">Tip: </span>
|
<span className="font-medium">Tip: </span>
|
||||||
{isDockerError
|
{isDockerError
|
||||||
? "Make sure Docker Desktop is running and try restarting the app."
|
? "Make sure Docker Desktop is running and try restarting the app."
|
||||||
|
: error.source === "dyad-app"
|
||||||
|
? "Try restarting the Dyad app or restarting your computer to see if that fixes the error."
|
||||||
: "Check if restarting the app fixes the error."}
|
: "Check if restarting the app fixes the error."}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AI Fix button at the bottom */}
|
{/* AI Fix button at the bottom */}
|
||||||
{!isDockerError && (
|
{!isDockerError && error.source === "preview-app" && (
|
||||||
<div className="mt-2 flex justify-end">
|
<div className="mt-2 flex justify-end">
|
||||||
<button
|
<button
|
||||||
disabled={isStreaming}
|
disabled={isStreaming}
|
||||||
@@ -217,7 +232,7 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
|||||||
payload?.message || payload?.reason
|
payload?.message || payload?.reason
|
||||||
}\nStack trace: ${stack}`;
|
}\nStack trace: ${stack}`;
|
||||||
console.error("Iframe error:", errorMessage);
|
console.error("Iframe error:", errorMessage);
|
||||||
setErrorMessage(errorMessage);
|
setErrorMessage({ message: errorMessage, source: "preview-app" });
|
||||||
setAppOutput((prev) => [
|
setAppOutput((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
@@ -230,7 +245,7 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
|||||||
} else if (type === "build-error-report") {
|
} else if (type === "build-error-report") {
|
||||||
console.debug(`Build error report: ${payload}`);
|
console.debug(`Build error report: ${payload}`);
|
||||||
const errorMessage = `${payload?.message} from file ${payload?.file}.\n\nSource code:\n${payload?.frame}`;
|
const errorMessage = `${payload?.message} from file ${payload?.file}.\n\nSource code:\n${payload?.frame}`;
|
||||||
setErrorMessage(errorMessage);
|
setErrorMessage({ message: errorMessage, source: "preview-app" });
|
||||||
setAppOutput((prev) => [
|
setAppOutput((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
@@ -542,7 +557,7 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
|
|||||||
onAIFix={() => {
|
onAIFix={() => {
|
||||||
if (selectedChatId) {
|
if (selectedChatId) {
|
||||||
streamMessage({
|
streamMessage({
|
||||||
prompt: `Fix error: ${errorMessage}`,
|
prompt: `Fix error: ${errorMessage?.message}`,
|
||||||
chatId: selectedChatId,
|
chatId: selectedChatId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,12 @@ export function useRunApp() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error running app ${appId}:`, error);
|
console.error(`Error running app ${appId}:`, error);
|
||||||
setPreviewErrorMessage(
|
setPreviewErrorMessage(
|
||||||
error instanceof Error ? error.message : error?.toString(),
|
error instanceof Error
|
||||||
|
? { message: error.message, source: "dyad-app" }
|
||||||
|
: {
|
||||||
|
message: error?.toString() || "Unknown error",
|
||||||
|
source: "dyad-app",
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -127,7 +132,12 @@ export function useRunApp() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error stopping app ${appId}:`, error);
|
console.error(`Error stopping app ${appId}:`, error);
|
||||||
setPreviewErrorMessage(
|
setPreviewErrorMessage(
|
||||||
error instanceof Error ? error.message : error?.toString(),
|
error instanceof Error
|
||||||
|
? { message: error.message, source: "dyad-app" }
|
||||||
|
: {
|
||||||
|
message: error?.toString() || "Unknown error",
|
||||||
|
source: "dyad-app",
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
@@ -186,7 +196,12 @@ export function useRunApp() {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error restarting app ${appId}:`, error);
|
console.error(`Error restarting app ${appId}:`, error);
|
||||||
setPreviewErrorMessage(
|
setPreviewErrorMessage(
|
||||||
error instanceof Error ? error.message : error?.toString(),
|
error instanceof Error
|
||||||
|
? { message: error.message, source: "dyad-app" }
|
||||||
|
: {
|
||||||
|
message: error?.toString() || "Unknown error",
|
||||||
|
source: "dyad-app",
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setPreviewPanelKey((prevKey) => prevKey + 1);
|
setPreviewPanelKey((prevKey) => prevKey + 1);
|
||||||
|
|||||||
@@ -152,13 +152,44 @@ async function executeAppLocalNode({
|
|||||||
if (!spawnedProcess.pid) {
|
if (!spawnedProcess.pid) {
|
||||||
// Attempt to capture any immediate errors if possible
|
// Attempt to capture any immediate errors if possible
|
||||||
let errorOutput = "";
|
let errorOutput = "";
|
||||||
spawnedProcess.stderr?.on("data", (data) => (errorOutput += data));
|
let spawnErr: any | null = null;
|
||||||
await new Promise((resolve) => spawnedProcess.on("error", resolve)); // Wait for error event
|
spawnedProcess.stderr?.on(
|
||||||
throw new Error(
|
"data",
|
||||||
`Failed to spawn process for app ${appId}. Error: ${
|
(data) => (errorOutput += data.toString()),
|
||||||
errorOutput || "Unknown spawn error"
|
);
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
spawnedProcess.once("error", (err) => {
|
||||||
|
spawnErr = err;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}); // Wait for error event
|
||||||
|
|
||||||
|
const details = [
|
||||||
|
spawnErr?.message ? `message=${spawnErr.message}` : null,
|
||||||
|
spawnErr?.code ? `code=${spawnErr.code}` : null,
|
||||||
|
spawnErr?.errno ? `errno=${spawnErr.errno}` : null,
|
||||||
|
spawnErr?.syscall ? `syscall=${spawnErr.syscall}` : null,
|
||||||
|
spawnErr?.path ? `path=${spawnErr.path}` : null,
|
||||||
|
spawnErr?.spawnargs
|
||||||
|
? `spawnargs=${JSON.stringify(spawnErr.spawnargs)}`
|
||||||
|
: null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
`Failed to spawn process for app ${appId}. Command="${command}", CWD="${appPath}", ${details}\nSTDERR:\n${
|
||||||
|
errorOutput || "(empty)"
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Failed to spawn process for app ${appId}.
|
||||||
|
Error output:
|
||||||
|
${errorOutput || "(empty)"}
|
||||||
|
Details: ${details || "n/a"}
|
||||||
|
`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment the counter and store the process reference with its ID
|
// Increment the counter and store the process reference with its ID
|
||||||
@@ -409,13 +440,40 @@ RUN npm install -g pnpm
|
|||||||
if (!process.pid) {
|
if (!process.pid) {
|
||||||
// Attempt to capture any immediate errors if possible
|
// Attempt to capture any immediate errors if possible
|
||||||
let errorOutput = "";
|
let errorOutput = "";
|
||||||
process.stderr?.on("data", (data) => (errorOutput += data));
|
let spawnErr: any = null;
|
||||||
await new Promise((resolve) => process.on("error", resolve)); // Wait for error event
|
process.stderr?.on("data", (data) => (errorOutput += data.toString()));
|
||||||
throw new Error(
|
await new Promise<void>((resolve) => {
|
||||||
`Failed to spawn Docker container for app ${appId}. Error: ${
|
process.once("error", (err) => {
|
||||||
errorOutput || "Unknown spawn error"
|
spawnErr = err;
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
}); // Wait for error event
|
||||||
|
|
||||||
|
const details = [
|
||||||
|
spawnErr?.message ? `message=${spawnErr.message}` : null,
|
||||||
|
spawnErr?.code ? `code=${spawnErr.code}` : null,
|
||||||
|
spawnErr?.errno ? `errno=${spawnErr.errno}` : null,
|
||||||
|
spawnErr?.syscall ? `syscall=${spawnErr.syscall}` : null,
|
||||||
|
spawnErr?.path ? `path=${spawnErr.path}` : null,
|
||||||
|
spawnErr?.spawnargs
|
||||||
|
? `spawnargs=${JSON.stringify(spawnErr.spawnargs)}`
|
||||||
|
: null,
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(", ");
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
`Failed to spawn Docker container for app ${appId}. ${details}\nSTDERR:\n${
|
||||||
|
errorOutput || "(empty)"
|
||||||
}`,
|
}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Failed to spawn Docker container for app ${appId}.
|
||||||
|
Details: ${details || "n/a"}
|
||||||
|
STDERR:
|
||||||
|
${errorOutput || "(empty)"}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment the counter and store the process reference with its ID
|
// Increment the counter and store the process reference with its ID
|
||||||
|
|||||||
Reference in New Issue
Block a user