diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx
index 02339a5..c167267 100644
--- a/src/components/chat/ChatInput.tsx
+++ b/src/components/chat/ChatInput.tsx
@@ -283,20 +283,7 @@ export function ChatInput({ chatId }: { chatId?: number }) {
}
function mapActionToButton(action: SuggestedAction) {
- const { restartApp } = useRunApp();
switch (action.id) {
- case "restart-app":
- return (
-
- );
default:
console.error(`Unsupported action: ${action.id}`);
return (
diff --git a/src/components/preview_panel/PreviewPanel.tsx b/src/components/preview_panel/PreviewPanel.tsx
index 598d53f..d037564 100644
--- a/src/components/preview_panel/PreviewPanel.tsx
+++ b/src/components/preview_panel/PreviewPanel.tsx
@@ -15,12 +15,23 @@ import {
ChevronUp,
Logs,
RefreshCw,
+ MoreVertical,
+ Trash2,
+ Cog,
+ CirclePower,
+ Power,
} from "lucide-react";
import { motion } from "framer-motion";
import { useEffect, useRef, useState, useCallback } from "react";
import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels";
import { Console } from "./Console";
import { useRunApp } from "@/hooks/useRunApp";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
type PreviewMode = "preview" | "code";
@@ -28,6 +39,7 @@ interface PreviewHeaderProps {
previewMode: PreviewMode;
setPreviewMode: (mode: PreviewMode) => void;
onRestart: () => void;
+ onCleanRestart: () => void;
}
interface ConsoleHeaderProps {
@@ -41,6 +53,7 @@ const PreviewHeader = ({
previewMode,
setPreviewMode,
onRestart,
+ onCleanRestart,
}: PreviewHeaderProps) => (
@@ -73,14 +86,37 @@ const PreviewHeader = ({
Code
-
+
+
+
+
+
+
+
+
+
+
+ Rebuild
+
+ Re-installs node_modules and restarts
+
+
+
+
+
+
);
@@ -126,6 +162,10 @@ export function PreviewPanel() {
restartApp();
}, [restartApp]);
+ const handleCleanRestart = useCallback(() => {
+ restartApp({ removeNodeModules: true });
+ }, [restartApp]);
+
useEffect(() => {
const previousAppId = runningAppIdRef.current;
@@ -176,6 +216,7 @@ export function PreviewPanel() {
previewMode={previewMode}
setPreviewMode={setPreviewMode}
onRestart={handleRestart}
+ onCleanRestart={handleCleanRestart}
/>
diff --git a/src/hooks/useRunApp.ts b/src/hooks/useRunApp.ts
index a653eed..f06043f 100644
--- a/src/hooks/useRunApp.ts
+++ b/src/hooks/useRunApp.ts
@@ -70,41 +70,56 @@ export function useRunApp() {
}
}, []);
- const restartApp = useCallback(async () => {
- if (appId === null) {
- return;
- }
- setLoading(true);
- try {
- const ipcClient = IpcClient.getInstance();
- console.debug("Restarting app", appId);
+ 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 });
- setAppOutput((prev) => [
- ...prev,
- { message: "Restarting app...", type: "stdout", appId },
- ]);
+ // Clear the URL and add restart message
+ setAppUrlObj({ appUrl: null, appId: null });
+ setAppOutput((prev) => [
+ ...prev,
+ { message: "Restarting app...", type: "stdout", appId },
+ ]);
- const app = await ipcClient.getApp(appId);
- setApp(app);
- await ipcClient.restartApp(appId, (output) => {
- setAppOutput((prev) => [...prev, output]);
- // Check if the output contains a localhost URL
- const urlMatch = output.message.match(/(https?:\/\/localhost:\d+\/?)/);
- if (urlMatch) {
- setAppUrlObj({ appUrl: urlMatch[1], appId });
- }
- });
- setError(null);
- } catch (error) {
- console.error(`Error restarting app ${appId}:`, error);
- setError(error instanceof Error ? error : new Error(String(error)));
- } finally {
- setPreviewPanelKey((prevKey) => prevKey + 1);
- setLoading(false);
- }
- }, []);
+ const app = await ipcClient.getApp(appId);
+ setApp(app);
+ await ipcClient.restartApp(
+ appId,
+ (output) => {
+ setAppOutput((prev) => [...prev, output]);
+ // Check if the output contains a localhost URL
+ const urlMatch = output.message.match(
+ /(https?:\/\/localhost:\d+\/?)/
+ );
+ if (urlMatch) {
+ setAppUrlObj({ appUrl: urlMatch[1], appId });
+ }
+ },
+ removeNodeModules
+ );
+ setError(null);
+ } catch (error) {
+ console.error(`Error restarting app ${appId}:`, error);
+ setError(error instanceof Error ? error : new Error(String(error)));
+ } finally {
+ setPreviewPanelKey((prevKey) => prevKey + 1);
+ setLoading(false);
+ }
+ },
+ [appId, setApp, setAppOutput, setAppUrlObj, setError, setPreviewPanelKey]
+ );
const refreshAppIframe = useCallback(async () => {
setPreviewPanelKey((prevKey) => prevKey + 1);
diff --git a/src/ipc/handlers/app_handlers.ts b/src/ipc/handlers/app_handlers.ts
index 1723668..3cbf73a 100644
--- a/src/ipc/handlers/app_handlers.ts
+++ b/src/ipc/handlers/app_handlers.ts
@@ -378,7 +378,10 @@ export function registerAppHandlers() {
"restart-app",
async (
event: Electron.IpcMainInvokeEvent,
- { appId }: { appId: number }
+ {
+ appId,
+ removeNodeModules,
+ }: { appId: number; removeNodeModules?: boolean }
) => {
logger.log(`Restarting app ${appId}`);
return withLock(appId, async () => {
@@ -410,6 +413,24 @@ export function registerAppHandlers() {
}
const appPath = getDyadAppPath(app.path);
+
+ // Remove node_modules if requested
+ if (removeNodeModules) {
+ const nodeModulesPath = path.join(appPath, "node_modules");
+ logger.log(
+ `Removing node_modules for app ${appId} at ${nodeModulesPath}`
+ );
+ if (fs.existsSync(nodeModulesPath)) {
+ await fsPromises.rm(nodeModulesPath, {
+ recursive: true,
+ force: true,
+ });
+ logger.log(`Successfully removed node_modules for app ${appId}`);
+ } else {
+ logger.log(`No node_modules directory found for app ${appId}`);
+ }
+ }
+
logger.debug(
`Executing app ${appId} in path ${app.path} after restart request`
); // Adjusted log
diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts
index d010c4b..5e74a53 100644
--- a/src/ipc/ipc_client.ts
+++ b/src/ipc/ipc_client.ts
@@ -317,10 +317,14 @@ export class IpcClient {
// Restart a running app
public async restartApp(
appId: number,
- onOutput: (output: AppOutput) => void
+ onOutput: (output: AppOutput) => void,
+ removeNodeModules?: boolean
): Promise<{ success: boolean }> {
try {
- const result = await this.ipcRenderer.invoke("restart-app", { appId });
+ const result = await this.ipcRenderer.invoke("restart-app", {
+ appId,
+ removeNodeModules,
+ });
this.appStreams.set(appId, { onOutput });
return result;
} catch (error) {