From 1327e64e52b8c4aa01c4ab6ff71bdef2794e4647 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Thu, 1 May 2025 22:48:43 -0700 Subject: [PATCH] Clear stale preview iframe errors (#65) --- src/atoms/appAtoms.ts | 2 + src/components/chat/ChatInput.tsx | 10 --- src/components/preview_panel/CodeView.tsx | 11 +--- .../preview_panel/PreviewIframe.tsx | 33 +++++----- src/components/preview_panel/PreviewPanel.tsx | 10 +-- src/hooks/useRunApp.ts | 61 +++++++++++++++---- src/ipc/ipc_client.ts | 4 +- src/ipc/ipc_types.ts | 1 + 8 files changed, 75 insertions(+), 57 deletions(-) diff --git a/src/atoms/appAtoms.ts b/src/atoms/appAtoms.ts index a18c456..3685758 100644 --- a/src/atoms/appAtoms.ts +++ b/src/atoms/appAtoms.ts @@ -19,3 +19,5 @@ export const userSettingsAtom = atom(null); export const envVarsAtom = atom>({}); export const previewPanelKeyAtom = atom(0); + +export const previewErrorMessageAtom = atom(undefined); diff --git a/src/components/chat/ChatInput.tsx b/src/components/chat/ChatInput.tsx index 0e02afc..d77e892 100644 --- a/src/components/chat/ChatInput.tsx +++ b/src/components/chat/ChatInput.tsx @@ -29,19 +29,13 @@ import { } from "@/atoms/chatAtoms"; import { atom, useAtom, useSetAtom, useAtomValue } from "jotai"; import { useStreamChat } from "@/hooks/useStreamChat"; -import { useChats } from "@/hooks/useChats"; import { selectedAppIdAtom } from "@/atoms/appAtoms"; -import { useLoadApp } from "@/hooks/useLoadApp"; import { Button } from "@/components/ui/button"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; import { useProposal } from "@/hooks/useProposal"; import { - CodeProposal, ActionProposal, Proposal, SuggestedAction, - ProposalResult, FileChange, SqlQuery, } from "@/lib/schemas"; @@ -69,7 +63,6 @@ export function ChatInput({ chatId }: { chatId?: number }) { const { settings, updateSettings, isAnyProviderSetup } = useSettings(); const { streamMessage, isStreaming, setIsStreaming, error, setError } = useStreamChat(); - const [selectedAppId] = useAtom(selectedAppIdAtom); const [showError, setShowError] = useState(true); const [isApproving, setIsApproving] = useState(false); // State for approving const [isRejecting, setIsRejecting] = useState(false); // State for rejecting @@ -77,8 +70,6 @@ export function ChatInput({ chatId }: { chatId?: number }) { const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom); const [showTokenBar, setShowTokenBar] = useAtom(showTokenBarAtom); - const { refreshAppIframe } = useRunApp(); - // Use the hook to fetch the proposal const { proposalResult, @@ -171,7 +162,6 @@ export function ChatInput({ chatId }: { chatId?: number }) { } finally { setIsApproving(false); setIsPreviewOpen(true); - refreshAppIframe(); // Keep same as handleReject refreshProposal(); diff --git a/src/components/preview_panel/CodeView.tsx b/src/components/preview_panel/CodeView.tsx index 0048dab..5e49b6c 100644 --- a/src/components/preview_panel/CodeView.tsx +++ b/src/components/preview_panel/CodeView.tsx @@ -13,12 +13,11 @@ interface App { export interface CodeViewProps { loading: boolean; - error: Error | null; app: App | null; } // Code view component that displays app files or status messages -export const CodeView = ({ loading, error, app }: CodeViewProps) => { +export const CodeView = ({ loading, app }: CodeViewProps) => { const selectedFile = useAtomValue(selectedFileAtom); const { refreshApp } = useLoadApp(app?.id ?? null); @@ -26,14 +25,6 @@ export const CodeView = ({ loading, error, app }: CodeViewProps) => { return
Loading files...
; } - if (error) { - return ( -
- Error loading files: {error.message} -
- ); - } - if (!app) { return (
No app selected
diff --git a/src/components/preview_panel/PreviewIframe.tsx b/src/components/preview_panel/PreviewIframe.tsx index b4cbd23..97e38bd 100644 --- a/src/components/preview_panel/PreviewIframe.tsx +++ b/src/components/preview_panel/PreviewIframe.tsx @@ -1,5 +1,10 @@ -import { selectedAppIdAtom, appUrlAtom, appOutputAtom } from "@/atoms/appAtoms"; -import { useAtomValue, useSetAtom } from "jotai"; +import { + selectedAppIdAtom, + appUrlAtom, + appOutputAtom, + previewErrorMessageAtom, +} from "@/atoms/appAtoms"; +import { useAtomValue, useSetAtom, useAtom } from "jotai"; import { useEffect, useRef, useState } from "react"; import { ArrowLeft, @@ -23,6 +28,7 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useSettings } from "@/hooks/useSettings"; +import { useRunApp } from "@/hooks/useRunApp"; interface ErrorBannerProps { error: string | undefined; @@ -78,23 +84,13 @@ const ErrorBanner = ({ error, onDismiss, onAIFix }: ErrorBannerProps) => { }; // Preview iframe component -export const PreviewIframe = ({ - loading, - loadingErrorMessage, -}: { - loading: boolean; - loadingErrorMessage: string | undefined; -}) => { +export const PreviewIframe = ({ loading }: { loading: boolean }) => { const selectedAppId = useAtomValue(selectedAppIdAtom); const { appUrl } = useAtomValue(appUrlAtom); const setAppOutput = useSetAtom(appOutputAtom); - const { app } = useLoadApp(selectedAppId); - // State to trigger iframe reload const [reloadKey, setReloadKey] = useState(0); - const [errorMessage, setErrorMessage] = useState( - loadingErrorMessage - ); + const [errorMessage, setErrorMessage] = useAtom(previewErrorMessageAtom); const setInputValue = useSetAtom(chatInputValueAtom); const [availableRoutes, setAvailableRoutes] = useState< Array<{ path: string; label: string }> @@ -179,6 +175,7 @@ export const PreviewIframe = ({ message: `Iframe error: ${errorMessage}`, type: "client-error", appId: selectedAppId!, + timestamp: Date.now(), }, ]); } else if (type === "pushState" || type === "replaceState") { @@ -204,7 +201,13 @@ export const PreviewIframe = ({ window.addEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage); - }, [navigationHistory, currentHistoryPosition, selectedAppId]); + }, [ + navigationHistory, + currentHistoryPosition, + selectedAppId, + errorMessage, + setErrorMessage, + ]); useEffect(() => { // Update navigation buttons state diff --git a/src/components/preview_panel/PreviewPanel.tsx b/src/components/preview_panel/PreviewPanel.tsx index d037564..e1e1d40 100644 --- a/src/components/preview_panel/PreviewPanel.tsx +++ b/src/components/preview_panel/PreviewPanel.tsx @@ -149,7 +149,7 @@ export function PreviewPanel() { const [previewMode, setPreviewMode] = useAtom(previewModeAtom); const selectedAppId = useAtomValue(selectedAppIdAtom); const [isConsoleOpen, setIsConsoleOpen] = useState(false); - const { runApp, stopApp, restartApp, error, loading, app } = useRunApp(); + const { runApp, stopApp, restartApp, loading, app } = useRunApp(); const runningAppIdRef = useRef(null); const key = useAtomValue(previewPanelKeyAtom); const appOutput = useAtomValue(appOutputAtom); @@ -223,13 +223,9 @@ export function PreviewPanel() {
{previewMode === "preview" ? ( - + ) : ( - + )}
diff --git a/src/hooks/useRunApp.ts b/src/hooks/useRunApp.ts index f06043f..41adf1f 100644 --- a/src/hooks/useRunApp.ts +++ b/src/hooks/useRunApp.ts @@ -5,6 +5,7 @@ import { appUrlAtom, currentAppAtom, previewPanelKeyAtom, + previewErrorMessageAtom, selectedAppIdAtom, } from "@/atoms/appAtoms"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; @@ -12,12 +13,12 @@ import { App } from "@/ipc/ipc_types"; export function useRunApp() { const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); const [app, setApp] = useAtom(currentAppAtom); const setAppOutput = useSetAtom(appOutputAtom); const [appUrlObj, setAppUrlObj] = useAtom(appUrlAtom); const setPreviewPanelKey = useSetAtom(previewPanelKeyAtom); const appId = useAtomValue(selectedAppIdAtom); + const setPreviewErrorMessage = useSetAtom(previewErrorMessageAtom); const runApp = useCallback(async (appId: number) => { setLoading(true); try { @@ -30,7 +31,12 @@ export function useRunApp() { } setAppOutput((prev) => [ ...prev, - { message: "Trying to restart app...", type: "stdout", appId }, + { + message: "Trying to restart app...", + type: "stdout", + appId, + timestamp: Date.now(), + }, ]); const app = await ipcClient.getApp(appId); setApp(app); @@ -42,10 +48,12 @@ export function useRunApp() { setAppUrlObj({ appUrl: urlMatch[1], appId }); } }); - setError(null); + setPreviewErrorMessage(undefined); } catch (error) { console.error(`Error running app ${appId}:`, error); - setError(error instanceof Error ? error : new Error(String(error))); + setPreviewErrorMessage( + error instanceof Error ? error.message : error?.toString() + ); } finally { setLoading(false); } @@ -61,15 +69,22 @@ export function useRunApp() { const ipcClient = IpcClient.getInstance(); await ipcClient.stopApp(appId); - setError(null); + setPreviewErrorMessage(undefined); } catch (error) { console.error(`Error stopping app ${appId}:`, error); - setError(error instanceof Error ? error : new Error(String(error))); + setPreviewErrorMessage( + error instanceof Error ? error.message : error?.toString() + ); } finally { setLoading(false); } }, []); + const onHotModuleReload = useCallback(() => { + setPreviewPanelKey((prevKey) => prevKey + 1); + setPreviewErrorMessage(undefined); + }, [setPreviewPanelKey, setPreviewErrorMessage]); + const restartApp = useCallback( async ({ removeNodeModules = false, @@ -90,7 +105,12 @@ export function useRunApp() { setAppUrlObj({ appUrl: null, appId: null }); setAppOutput((prev) => [ ...prev, - { message: "Restarting app...", type: "stdout", appId }, + { + message: "Restarting app...", + type: "stdout", + appId, + timestamp: Date.now(), + }, ]); const app = await ipcClient.getApp(appId); @@ -99,6 +119,13 @@ export function useRunApp() { appId, (output) => { setAppOutput((prev) => [...prev, output]); + if ( + output.message.includes("hmr update") && + output.message.includes("[vite]") + ) { + onHotModuleReload(); + return; + } // Check if the output contains a localhost URL const urlMatch = output.message.match( /(https?:\/\/localhost:\d+\/?)/ @@ -109,22 +136,30 @@ export function useRunApp() { }, removeNodeModules ); - setError(null); } catch (error) { console.error(`Error restarting app ${appId}:`, error); - setError(error instanceof Error ? error : new Error(String(error))); + setPreviewErrorMessage( + error instanceof Error ? error.message : error?.toString() + ); } finally { setPreviewPanelKey((prevKey) => prevKey + 1); setLoading(false); } }, - [appId, setApp, setAppOutput, setAppUrlObj, setError, setPreviewPanelKey] + [appId, setApp, setAppOutput, setAppUrlObj, setPreviewPanelKey] ); const refreshAppIframe = useCallback(async () => { setPreviewPanelKey((prevKey) => prevKey + 1); - setError(null); - }, [setPreviewPanelKey, setError]); + setPreviewErrorMessage(undefined); + }, [setPreviewPanelKey, setPreviewErrorMessage]); - return { loading, error, runApp, stopApp, restartApp, app, refreshAppIframe }; + return { + loading, + runApp, + stopApp, + restartApp, + app, + refreshAppIframe, + }; } diff --git a/src/ipc/ipc_client.ts b/src/ipc/ipc_client.ts index 3c63c40..a37f159 100644 --- a/src/ipc/ipc_client.ts +++ b/src/ipc/ipc_client.ts @@ -99,10 +99,10 @@ export class IpcClient { "message" in data && "appId" in data ) { - const { type, message, appId } = data as AppOutput; + const { type, message, appId } = data as unknown as AppOutput; const callbacks = this.appStreams.get(appId); if (callbacks) { - callbacks.onOutput({ type, message, appId }); + callbacks.onOutput({ type, message, appId, timestamp: Date.now() }); } } else { showError(new Error(`[IPC] Invalid app output data received: ${data}`)); diff --git a/src/ipc/ipc_types.ts b/src/ipc/ipc_types.ts index 4e03e66..09ae89e 100644 --- a/src/ipc/ipc_types.ts +++ b/src/ipc/ipc_types.ts @@ -1,6 +1,7 @@ export interface AppOutput { type: "stdout" | "stderr" | "info" | "client-error"; message: string; + timestamp: number; appId: number; }