<!-- 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>
235 lines
6.5 KiB
TypeScript
235 lines
6.5 KiB
TypeScript
import { useCallback } from "react";
|
|
import { atom } from "jotai";
|
|
import { IpcClient } from "@/ipc/ipc_client";
|
|
import {
|
|
appOutputAtom,
|
|
appUrlAtom,
|
|
currentAppAtom,
|
|
previewPanelKeyAtom,
|
|
previewErrorMessageAtom,
|
|
selectedAppIdAtom,
|
|
} from "@/atoms/appAtoms";
|
|
import { useAtom, useAtomValue, useSetAtom } from "jotai";
|
|
import { AppOutput } from "@/ipc/ipc_types";
|
|
import { showInputRequest } from "@/lib/toast";
|
|
|
|
const useRunAppLoadingAtom = atom(false);
|
|
|
|
export function useRunApp() {
|
|
const [loading, setLoading] = useAtom(useRunAppLoadingAtom);
|
|
const [app, setApp] = useAtom(currentAppAtom);
|
|
const setAppOutput = useSetAtom(appOutputAtom);
|
|
const [, setAppUrlObj] = useAtom(appUrlAtom);
|
|
const setPreviewPanelKey = useSetAtom(previewPanelKeyAtom);
|
|
const appId = useAtomValue(selectedAppIdAtom);
|
|
const setPreviewErrorMessage = useSetAtom(previewErrorMessageAtom);
|
|
|
|
const processProxyServerOutput = (output: AppOutput) => {
|
|
const matchesProxyServerStart = output.message.includes(
|
|
"[dyad-proxy-server]started=[",
|
|
);
|
|
if (matchesProxyServerStart) {
|
|
// Extract both proxy URL and original URL using regex
|
|
const proxyUrlMatch = output.message.match(
|
|
/\[dyad-proxy-server\]started=\[(.*?)\]/,
|
|
);
|
|
const originalUrlMatch = output.message.match(/original=\[(.*?)\]/);
|
|
|
|
if (proxyUrlMatch && proxyUrlMatch[1]) {
|
|
const proxyUrl = proxyUrlMatch[1];
|
|
const originalUrl = originalUrlMatch && originalUrlMatch[1];
|
|
setAppUrlObj({
|
|
appUrl: proxyUrl,
|
|
appId: output.appId,
|
|
originalUrl: originalUrl!,
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
const processAppOutput = useCallback(
|
|
(output: AppOutput) => {
|
|
// Handle input requests specially
|
|
if (output.type === "input-requested") {
|
|
showInputRequest(output.message, async (response) => {
|
|
try {
|
|
const ipcClient = IpcClient.getInstance();
|
|
await ipcClient.respondToAppInput({
|
|
appId: output.appId,
|
|
response,
|
|
});
|
|
} catch (error) {
|
|
console.error("Failed to respond to app input:", error);
|
|
}
|
|
});
|
|
return; // Don't add to regular output
|
|
}
|
|
|
|
// Add to regular app output
|
|
setAppOutput((prev) => [...prev, output]);
|
|
|
|
// Process proxy server output
|
|
processProxyServerOutput(output);
|
|
},
|
|
[setAppOutput],
|
|
);
|
|
const runApp = useCallback(
|
|
async (appId: number) => {
|
|
setLoading(true);
|
|
try {
|
|
const ipcClient = IpcClient.getInstance();
|
|
console.debug("Running app", appId);
|
|
|
|
// Clear the URL and add restart message
|
|
setAppUrlObj((prevAppUrlObj) => {
|
|
if (prevAppUrlObj?.appId !== appId) {
|
|
return { appUrl: null, appId: null, originalUrl: null };
|
|
}
|
|
return prevAppUrlObj; // No change needed
|
|
});
|
|
|
|
setAppOutput((prev) => [
|
|
...prev,
|
|
{
|
|
message: "Trying to restart app...",
|
|
type: "stdout",
|
|
appId,
|
|
timestamp: Date.now(),
|
|
},
|
|
]);
|
|
const app = await ipcClient.getApp(appId);
|
|
setApp(app);
|
|
await ipcClient.runApp(appId, processAppOutput);
|
|
setPreviewErrorMessage(undefined);
|
|
} catch (error) {
|
|
console.error(`Error running app ${appId}:`, error);
|
|
setPreviewErrorMessage(
|
|
error instanceof Error
|
|
? { message: error.message, source: "dyad-app" }
|
|
: {
|
|
message: error?.toString() || "Unknown error",
|
|
source: "dyad-app",
|
|
},
|
|
);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
},
|
|
[processAppOutput],
|
|
);
|
|
|
|
const stopApp = useCallback(async (appId: number) => {
|
|
if (appId === null) {
|
|
return;
|
|
}
|
|
|
|
setLoading(true);
|
|
try {
|
|
const ipcClient = IpcClient.getInstance();
|
|
await ipcClient.stopApp(appId);
|
|
|
|
setPreviewErrorMessage(undefined);
|
|
} catch (error) {
|
|
console.error(`Error stopping app ${appId}:`, error);
|
|
setPreviewErrorMessage(
|
|
error instanceof Error
|
|
? { message: error.message, source: "dyad-app" }
|
|
: {
|
|
message: error?.toString() || "Unknown error",
|
|
source: "dyad-app",
|
|
},
|
|
);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const onHotModuleReload = useCallback(() => {
|
|
setPreviewPanelKey((prevKey) => prevKey + 1);
|
|
}, [setPreviewPanelKey]);
|
|
|
|
const restartApp = useCallback(
|
|
async ({
|
|
removeNodeModules = false,
|
|
}: { removeNodeModules?: boolean } = {}) => {
|
|
if (appId === null) {
|
|
return;
|
|
}
|
|
setLoading(true);
|
|
try {
|
|
const ipcClient = IpcClient.getInstance();
|
|
console.debug(
|
|
"Restarting app",
|
|
appId,
|
|
removeNodeModules ? "with node_modules cleanup" : "",
|
|
);
|
|
|
|
// Clear the URL and add restart message
|
|
setAppUrlObj({ appUrl: null, appId: null, originalUrl: null });
|
|
setAppOutput((prev) => [
|
|
...prev,
|
|
{
|
|
message: "Restarting app...",
|
|
type: "stdout",
|
|
appId,
|
|
timestamp: Date.now(),
|
|
},
|
|
]);
|
|
|
|
const app = await ipcClient.getApp(appId);
|
|
setApp(app);
|
|
await ipcClient.restartApp(
|
|
appId,
|
|
(output) => {
|
|
// Handle HMR updates before processing
|
|
if (
|
|
output.message.includes("hmr update") &&
|
|
output.message.includes("[vite]")
|
|
) {
|
|
onHotModuleReload();
|
|
}
|
|
// Process normally (including input requests)
|
|
processAppOutput(output);
|
|
},
|
|
removeNodeModules,
|
|
);
|
|
} catch (error) {
|
|
console.error(`Error restarting app ${appId}:`, error);
|
|
setPreviewErrorMessage(
|
|
error instanceof Error
|
|
? { message: error.message, source: "dyad-app" }
|
|
: {
|
|
message: error?.toString() || "Unknown error",
|
|
source: "dyad-app",
|
|
},
|
|
);
|
|
} finally {
|
|
setPreviewPanelKey((prevKey) => prevKey + 1);
|
|
setLoading(false);
|
|
}
|
|
},
|
|
[
|
|
appId,
|
|
setApp,
|
|
setAppOutput,
|
|
setAppUrlObj,
|
|
setPreviewPanelKey,
|
|
processAppOutput,
|
|
onHotModuleReload,
|
|
],
|
|
);
|
|
|
|
const refreshAppIframe = useCallback(async () => {
|
|
setPreviewPanelKey((prevKey) => prevKey + 1);
|
|
}, [setPreviewPanelKey]);
|
|
|
|
return {
|
|
loading,
|
|
runApp,
|
|
stopApp,
|
|
restartApp,
|
|
app,
|
|
refreshAppIframe,
|
|
};
|
|
}
|