Clear stale preview iframe errors (#65)
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`));
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user