diff --git a/e2e-tests/helpers/test_helper.ts b/e2e-tests/helpers/test_helper.ts
index 9065d9c..adb3a94 100644
--- a/e2e-tests/helpers/test_helper.ts
+++ b/e2e-tests/helpers/test_helper.ts
@@ -484,6 +484,10 @@ export class PageObject {
return this.page.getByText("Loading app preview...");
}
+ locateStartingAppPreview() {
+ return this.page.getByText("Starting up your app...");
+ }
+
getPreviewIframeElement() {
return this.page.getByTestId("preview-iframe-element");
}
diff --git a/src/app/TitleBar.tsx b/src/app/TitleBar.tsx
index ee0c348..5a9ce1c 100644
--- a/src/app/TitleBar.tsx
+++ b/src/app/TitleBar.tsx
@@ -1,7 +1,7 @@
import { useAtom } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useLoadApps } from "@/hooks/useLoadApps";
-import { useRouter } from "@tanstack/react-router";
+import { useRouter, useLocation } from "@tanstack/react-router";
import { useSettings } from "@/hooks/useSettings";
import { Button } from "@/components/ui/button";
// @ts-ignore
@@ -20,11 +20,13 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
+import { PreviewHeader } from "@/components/preview_panel/PreviewHeader";
export const TitleBar = () => {
const [selectedAppId] = useAtom(selectedAppIdAtom);
const { apps } = useLoadApps();
const { navigate } = useRouter();
+ const location = useLocation();
const { settings, refreshSettings } = useSettings();
const [isSuccessDialogOpen, setIsSuccessDialogOpen] = useState(false);
const [showWindowControls, setShowWindowControls] = useState(false);
@@ -90,6 +92,14 @@ export const TitleBar = () => {
{displayText}
{isDyadPro && }
+
+ {/* Preview Header */}
+ {location.pathname === "/chat" && (
+
+ )}
+
{showWindowControls && }
diff --git a/src/components/preview_panel/PreviewHeader.tsx b/src/components/preview_panel/PreviewHeader.tsx
new file mode 100644
index 0000000..21fc738
--- /dev/null
+++ b/src/components/preview_panel/PreviewHeader.tsx
@@ -0,0 +1,214 @@
+import { useAtom, useAtomValue } from "jotai";
+import { previewModeAtom, selectedAppIdAtom } from "../../atoms/appAtoms";
+import { IpcClient } from "@/ipc/ipc_client";
+
+import {
+ Eye,
+ Code,
+ MoreVertical,
+ Cog,
+ Trash2,
+ AlertTriangle,
+} from "lucide-react";
+import { motion } from "framer-motion";
+import { useEffect, useRef, useState, useCallback } from "react";
+
+import { useRunApp } from "@/hooks/useRunApp";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import { showError, showSuccess } from "@/lib/toast";
+import { useMutation } from "@tanstack/react-query";
+import { useCheckProblems } from "@/hooks/useCheckProblems";
+import { isPreviewOpenAtom } from "@/atoms/viewAtoms";
+
+export type PreviewMode = "preview" | "code" | "problems";
+
+// Preview Header component with preview mode toggle
+export const PreviewHeader = () => {
+ const [previewMode, setPreviewMode] = useAtom(previewModeAtom);
+ const [isPreviewOpen, setIsPreviewOpen] = useAtom(isPreviewOpenAtom);
+ const selectedAppId = useAtomValue(selectedAppIdAtom);
+ const previewRef = useRef(null);
+ const codeRef = useRef(null);
+ const problemsRef = useRef(null);
+ const [indicatorStyle, setIndicatorStyle] = useState({ left: 0, width: 0 });
+ const { problemReport } = useCheckProblems(selectedAppId);
+ const { restartApp, refreshAppIframe } = useRunApp();
+
+ const selectPanel = (panel: PreviewMode) => {
+ if (previewMode === panel) {
+ setIsPreviewOpen(!isPreviewOpen);
+ } else {
+ setPreviewMode(panel);
+ setIsPreviewOpen(true);
+ }
+ };
+
+ const onCleanRestart = useCallback(() => {
+ restartApp({ removeNodeModules: true });
+ }, [restartApp]);
+
+ const useClearSessionData = () => {
+ return useMutation({
+ mutationFn: () => {
+ const ipcClient = IpcClient.getInstance();
+ return ipcClient.clearSessionData();
+ },
+ onSuccess: async () => {
+ await refreshAppIframe();
+ showSuccess("Preview data cleared");
+ },
+ onError: (error) => {
+ showError(`Error clearing preview data: ${error}`);
+ },
+ });
+ };
+
+ const { mutate: clearSessionData } = useClearSessionData();
+
+ const onClearSessionData = useCallback(() => {
+ clearSessionData();
+ }, [clearSessionData]);
+
+ // Get the problem count for the selected app
+ const problemCount = problemReport ? problemReport.problems.length : 0;
+
+ // Format the problem count for display
+ const formatProblemCount = (count: number): string => {
+ if (count === 0) return "";
+ if (count > 100) return "100+";
+ return count.toString();
+ };
+
+ const displayCount = formatProblemCount(problemCount);
+
+ // Update indicator position when mode changes
+ useEffect(() => {
+ const updateIndicator = () => {
+ let targetRef: React.RefObject;
+
+ switch (previewMode) {
+ case "preview":
+ targetRef = previewRef;
+ break;
+ case "code":
+ targetRef = codeRef;
+ break;
+ case "problems":
+ targetRef = problemsRef;
+ break;
+ default:
+ return;
+ }
+
+ if (targetRef.current) {
+ const button = targetRef.current;
+ const container = button.parentElement;
+ if (container) {
+ const containerRect = container.getBoundingClientRect();
+ const buttonRect = button.getBoundingClientRect();
+ const left = buttonRect.left - containerRect.left;
+ const width = buttonRect.width;
+
+ setIndicatorStyle({ left, width });
+ if (!isPreviewOpen) {
+ setIndicatorStyle({ left: left, width: 0 });
+ }
+ }
+ }
+ };
+
+ // Small delay to ensure DOM is updated
+ const timeoutId = setTimeout(updateIndicator, 10);
+ return () => clearTimeout(timeoutId);
+ }, [previewMode, displayCount, isPreviewOpen]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Rebuild
+
+ Re-installs node_modules and restarts
+
+
+
+
+
+
+ Clear Cache
+
+ Clears cookies and local storage and other app cache
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/components/preview_panel/PreviewIframe.tsx b/src/components/preview_panel/PreviewIframe.tsx
index ec75848..d216383 100644
--- a/src/components/preview_panel/PreviewIframe.tsx
+++ b/src/components/preview_panel/PreviewIframe.tsx
@@ -18,6 +18,7 @@ import {
Lightbulb,
ChevronRight,
MousePointerClick,
+ Power,
} from "lucide-react";
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
import { IpcClient } from "@/ipc/ipc_client";
@@ -38,6 +39,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
+import { useRunApp } from "@/hooks/useRunApp";
interface ErrorBannerProps {
error: string | undefined;
@@ -129,7 +131,7 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
const [availableRoutes, setAvailableRoutes] = useState<
Array<{ path: string; label: string }>
>([]);
-
+ const { restartApp } = useRunApp();
// Load router related files to extract routes
const { content: routerContent } = useLoadAppFile(
selectedAppId,
@@ -423,6 +425,10 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
);
}
+ const onRestart = () => {
+ restartApp();
+ };
+
return (
{/* Browser-style header */}
@@ -519,6 +525,14 @@ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
{/* Action Buttons */}
+