Clear stale preview iframe errors (#65)

This commit is contained in:
Will Chen
2025-05-01 22:48:43 -07:00
committed by GitHub
parent 79b7f865fc
commit 1327e64e52
8 changed files with 75 additions and 57 deletions

View File

@@ -19,3 +19,5 @@ export const userSettingsAtom = atom<UserSettings | null>(null);
export const envVarsAtom = atom<Record<string, string | undefined>>({}); 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);

View File

@@ -29,19 +29,13 @@ import {
} from "@/atoms/chatAtoms"; } from "@/atoms/chatAtoms";
import { atom, useAtom, useSetAtom, useAtomValue } from "jotai"; import { atom, useAtom, useSetAtom, useAtomValue } from "jotai";
import { useStreamChat } from "@/hooks/useStreamChat"; import { useStreamChat } from "@/hooks/useStreamChat";
import { useChats } from "@/hooks/useChats";
import { selectedAppIdAtom } from "@/atoms/appAtoms"; import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useLoadApp } from "@/hooks/useLoadApp";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { useProposal } from "@/hooks/useProposal"; import { useProposal } from "@/hooks/useProposal";
import { import {
CodeProposal,
ActionProposal, ActionProposal,
Proposal, Proposal,
SuggestedAction, SuggestedAction,
ProposalResult,
FileChange, FileChange,
SqlQuery, SqlQuery,
} from "@/lib/schemas"; } from "@/lib/schemas";
@@ -69,7 +63,6 @@ export function ChatInput({ chatId }: { chatId?: number }) {
const { settings, updateSettings, isAnyProviderSetup } = useSettings(); const { settings, updateSettings, isAnyProviderSetup } = useSettings();
const { streamMessage, isStreaming, setIsStreaming, error, setError } = const { streamMessage, isStreaming, setIsStreaming, error, setError } =
useStreamChat(); useStreamChat();
const [selectedAppId] = useAtom(selectedAppIdAtom);
const [showError, setShowError] = useState(true); const [showError, setShowError] = useState(true);
const [isApproving, setIsApproving] = useState(false); // State for approving const [isApproving, setIsApproving] = useState(false); // State for approving
const [isRejecting, setIsRejecting] = useState(false); // State for rejecting const [isRejecting, setIsRejecting] = useState(false); // State for rejecting
@@ -77,8 +70,6 @@ export function ChatInput({ chatId }: { chatId?: number }) {
const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom); const setIsPreviewOpen = useSetAtom(isPreviewOpenAtom);
const [showTokenBar, setShowTokenBar] = useAtom(showTokenBarAtom); const [showTokenBar, setShowTokenBar] = useAtom(showTokenBarAtom);
const { refreshAppIframe } = useRunApp();
// Use the hook to fetch the proposal // Use the hook to fetch the proposal
const { const {
proposalResult, proposalResult,
@@ -171,7 +162,6 @@ export function ChatInput({ chatId }: { chatId?: number }) {
} finally { } finally {
setIsApproving(false); setIsApproving(false);
setIsPreviewOpen(true); setIsPreviewOpen(true);
refreshAppIframe();
// Keep same as handleReject // Keep same as handleReject
refreshProposal(); refreshProposal();

View File

@@ -13,12 +13,11 @@ interface App {
export interface CodeViewProps { export interface CodeViewProps {
loading: boolean; loading: boolean;
error: Error | null;
app: App | null; app: App | null;
} }
// Code view component that displays app files or status messages // 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 selectedFile = useAtomValue(selectedFileAtom);
const { refreshApp } = useLoadApp(app?.id ?? null); const { refreshApp } = useLoadApp(app?.id ?? null);
@@ -26,14 +25,6 @@ export const CodeView = ({ loading, error, app }: CodeViewProps) => {
return <div className="text-center py-4">Loading files...</div>; return <div className="text-center py-4">Loading files...</div>;
} }
if (error) {
return (
<div className="text-center py-4 text-red-500">
Error loading files: {error.message}
</div>
);
}
if (!app) { if (!app) {
return ( return (
<div className="text-center py-4 text-gray-500">No app selected</div> <div className="text-center py-4 text-gray-500">No app selected</div>

View File

@@ -1,5 +1,10 @@
import { selectedAppIdAtom, appUrlAtom, appOutputAtom } from "@/atoms/appAtoms"; import {
import { useAtomValue, useSetAtom } from "jotai"; selectedAppIdAtom,
appUrlAtom,
appOutputAtom,
previewErrorMessageAtom,
} from "@/atoms/appAtoms";
import { useAtomValue, useSetAtom, useAtom } from "jotai";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { import {
ArrowLeft, ArrowLeft,
@@ -23,6 +28,7 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import { useSettings } from "@/hooks/useSettings"; import { useSettings } from "@/hooks/useSettings";
import { useRunApp } from "@/hooks/useRunApp";
interface ErrorBannerProps { interface ErrorBannerProps {
error: string | undefined; error: string | undefined;
@@ -78,23 +84,13 @@ const ErrorBanner = ({ error, onDismiss, onAIFix }: ErrorBannerProps) => {
}; };
// Preview iframe component // Preview iframe component
export const PreviewIframe = ({ export const PreviewIframe = ({ loading }: { loading: boolean }) => {
loading,
loadingErrorMessage,
}: {
loading: boolean;
loadingErrorMessage: string | undefined;
}) => {
const selectedAppId = useAtomValue(selectedAppIdAtom); const selectedAppId = useAtomValue(selectedAppIdAtom);
const { appUrl } = useAtomValue(appUrlAtom); const { appUrl } = useAtomValue(appUrlAtom);
const setAppOutput = useSetAtom(appOutputAtom); const setAppOutput = useSetAtom(appOutputAtom);
const { app } = useLoadApp(selectedAppId);
// State to trigger iframe reload // State to trigger iframe reload
const [reloadKey, setReloadKey] = useState(0); const [reloadKey, setReloadKey] = useState(0);
const [errorMessage, setErrorMessage] = useState<string | undefined>( const [errorMessage, setErrorMessage] = useAtom(previewErrorMessageAtom);
loadingErrorMessage
);
const setInputValue = useSetAtom(chatInputValueAtom); const setInputValue = useSetAtom(chatInputValueAtom);
const [availableRoutes, setAvailableRoutes] = useState< const [availableRoutes, setAvailableRoutes] = useState<
Array<{ path: string; label: string }> Array<{ path: string; label: string }>
@@ -179,6 +175,7 @@ export const PreviewIframe = ({
message: `Iframe error: ${errorMessage}`, message: `Iframe error: ${errorMessage}`,
type: "client-error", type: "client-error",
appId: selectedAppId!, appId: selectedAppId!,
timestamp: Date.now(),
}, },
]); ]);
} else if (type === "pushState" || type === "replaceState") { } else if (type === "pushState" || type === "replaceState") {
@@ -204,7 +201,13 @@ export const PreviewIframe = ({
window.addEventListener("message", handleMessage); window.addEventListener("message", handleMessage);
return () => window.removeEventListener("message", handleMessage); return () => window.removeEventListener("message", handleMessage);
}, [navigationHistory, currentHistoryPosition, selectedAppId]); }, [
navigationHistory,
currentHistoryPosition,
selectedAppId,
errorMessage,
setErrorMessage,
]);
useEffect(() => { useEffect(() => {
// Update navigation buttons state // Update navigation buttons state

View File

@@ -149,7 +149,7 @@ export function PreviewPanel() {
const [previewMode, setPreviewMode] = useAtom(previewModeAtom); const [previewMode, setPreviewMode] = useAtom(previewModeAtom);
const selectedAppId = useAtomValue(selectedAppIdAtom); const selectedAppId = useAtomValue(selectedAppIdAtom);
const [isConsoleOpen, setIsConsoleOpen] = useState(false); const [isConsoleOpen, setIsConsoleOpen] = useState(false);
const { runApp, stopApp, restartApp, error, loading, app } = useRunApp(); const { runApp, stopApp, restartApp, loading, app } = useRunApp();
const runningAppIdRef = useRef<number | null>(null); const runningAppIdRef = useRef<number | null>(null);
const key = useAtomValue(previewPanelKeyAtom); const key = useAtomValue(previewPanelKeyAtom);
const appOutput = useAtomValue(appOutputAtom); const appOutput = useAtomValue(appOutputAtom);
@@ -223,13 +223,9 @@ export function PreviewPanel() {
<Panel id="content" minSize={30}> <Panel id="content" minSize={30}>
<div className="h-full overflow-y-auto"> <div className="h-full overflow-y-auto">
{previewMode === "preview" ? ( {previewMode === "preview" ? (
<PreviewIframe <PreviewIframe key={key} loading={loading} />
key={key}
loading={loading}
loadingErrorMessage={error?.message}
/>
) : ( ) : (
<CodeView loading={loading} error={error} app={app} /> <CodeView loading={loading} app={app} />
)} )}
</div> </div>
</Panel> </Panel>

View File

@@ -5,6 +5,7 @@ import {
appUrlAtom, appUrlAtom,
currentAppAtom, currentAppAtom,
previewPanelKeyAtom, previewPanelKeyAtom,
previewErrorMessageAtom,
selectedAppIdAtom, selectedAppIdAtom,
} from "@/atoms/appAtoms"; } from "@/atoms/appAtoms";
import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useAtom, useAtomValue, useSetAtom } from "jotai";
@@ -12,12 +13,12 @@ import { App } from "@/ipc/ipc_types";
export function useRunApp() { export function useRunApp() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const [app, setApp] = useAtom(currentAppAtom); const [app, setApp] = useAtom(currentAppAtom);
const setAppOutput = useSetAtom(appOutputAtom); const setAppOutput = useSetAtom(appOutputAtom);
const [appUrlObj, setAppUrlObj] = useAtom(appUrlAtom); const [appUrlObj, setAppUrlObj] = useAtom(appUrlAtom);
const setPreviewPanelKey = useSetAtom(previewPanelKeyAtom); const setPreviewPanelKey = useSetAtom(previewPanelKeyAtom);
const appId = useAtomValue(selectedAppIdAtom); const appId = useAtomValue(selectedAppIdAtom);
const setPreviewErrorMessage = useSetAtom(previewErrorMessageAtom);
const runApp = useCallback(async (appId: number) => { const runApp = useCallback(async (appId: number) => {
setLoading(true); setLoading(true);
try { try {
@@ -30,7 +31,12 @@ export function useRunApp() {
} }
setAppOutput((prev) => [ setAppOutput((prev) => [
...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); const app = await ipcClient.getApp(appId);
setApp(app); setApp(app);
@@ -42,10 +48,12 @@ export function useRunApp() {
setAppUrlObj({ appUrl: urlMatch[1], appId }); setAppUrlObj({ appUrl: urlMatch[1], appId });
} }
}); });
setError(null); setPreviewErrorMessage(undefined);
} catch (error) { } catch (error) {
console.error(`Error running app ${appId}:`, 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 { } finally {
setLoading(false); setLoading(false);
} }
@@ -61,15 +69,22 @@ export function useRunApp() {
const ipcClient = IpcClient.getInstance(); const ipcClient = IpcClient.getInstance();
await ipcClient.stopApp(appId); await ipcClient.stopApp(appId);
setError(null); setPreviewErrorMessage(undefined);
} catch (error) { } catch (error) {
console.error(`Error stopping app ${appId}:`, 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 { } finally {
setLoading(false); setLoading(false);
} }
}, []); }, []);
const onHotModuleReload = useCallback(() => {
setPreviewPanelKey((prevKey) => prevKey + 1);
setPreviewErrorMessage(undefined);
}, [setPreviewPanelKey, setPreviewErrorMessage]);
const restartApp = useCallback( const restartApp = useCallback(
async ({ async ({
removeNodeModules = false, removeNodeModules = false,
@@ -90,7 +105,12 @@ export function useRunApp() {
setAppUrlObj({ appUrl: null, appId: null }); setAppUrlObj({ appUrl: null, appId: null });
setAppOutput((prev) => [ setAppOutput((prev) => [
...prev, ...prev,
{ message: "Restarting app...", type: "stdout", appId }, {
message: "Restarting app...",
type: "stdout",
appId,
timestamp: Date.now(),
},
]); ]);
const app = await ipcClient.getApp(appId); const app = await ipcClient.getApp(appId);
@@ -99,6 +119,13 @@ export function useRunApp() {
appId, appId,
(output) => { (output) => {
setAppOutput((prev) => [...prev, 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 // Check if the output contains a localhost URL
const urlMatch = output.message.match( const urlMatch = output.message.match(
/(https?:\/\/localhost:\d+\/?)/ /(https?:\/\/localhost:\d+\/?)/
@@ -109,22 +136,30 @@ export function useRunApp() {
}, },
removeNodeModules removeNodeModules
); );
setError(null);
} catch (error) { } catch (error) {
console.error(`Error restarting app ${appId}:`, 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 { } finally {
setPreviewPanelKey((prevKey) => prevKey + 1); setPreviewPanelKey((prevKey) => prevKey + 1);
setLoading(false); setLoading(false);
} }
}, },
[appId, setApp, setAppOutput, setAppUrlObj, setError, setPreviewPanelKey] [appId, setApp, setAppOutput, setAppUrlObj, setPreviewPanelKey]
); );
const refreshAppIframe = useCallback(async () => { const refreshAppIframe = useCallback(async () => {
setPreviewPanelKey((prevKey) => prevKey + 1); setPreviewPanelKey((prevKey) => prevKey + 1);
setError(null); setPreviewErrorMessage(undefined);
}, [setPreviewPanelKey, setError]); }, [setPreviewPanelKey, setPreviewErrorMessage]);
return { loading, error, runApp, stopApp, restartApp, app, refreshAppIframe }; return {
loading,
runApp,
stopApp,
restartApp,
app,
refreshAppIframe,
};
} }

View File

@@ -99,10 +99,10 @@ export class IpcClient {
"message" in data && "message" in data &&
"appId" 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); const callbacks = this.appStreams.get(appId);
if (callbacks) { if (callbacks) {
callbacks.onOutput({ type, message, appId }); callbacks.onOutput({ type, message, appId, timestamp: Date.now() });
} }
} else { } else {
showError(new Error(`[IPC] Invalid app output data received: ${data}`)); showError(new Error(`[IPC] Invalid app output data received: ${data}`));

View File

@@ -1,6 +1,7 @@
export interface AppOutput { export interface AppOutput {
type: "stdout" | "stderr" | "info" | "client-error"; type: "stdout" | "stderr" | "info" | "client-error";
message: string; message: string;
timestamp: number;
appId: number; appId: number;
} }