diff --git a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx
index 775c30c..cebec62 100644
--- a/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx
+++ b/vibn-frontend/app/[workspace]/project/[projectId]/(home)/preview/page.tsx
@@ -155,6 +155,8 @@ export default function PreviewTab() {
const refreshKey = usePreviewToolbarStore((s) => s.refreshKey);
const currentPath = usePreviewToolbarStore((s) => s.currentPath);
+ const [isForceStarting, setIsForceStarting] = useState(false);
+
// When the user clicks the manual refresh button in the toolbar, we don't
// just want to reload the iframe — we also want to trigger the same ghost/zombie
// check as the initial mount, in case the server died while they were looking at it.
@@ -163,9 +165,13 @@ export default function PreviewTab() {
if (refreshKey === prevRefreshKeyRef.current) return;
prevRefreshKeyRef.current = refreshKey;
- // Reset the ensure called flag so the ensure effect (below) fires again.
- ensureCalledRef.current = false;
- }, [refreshKey]);
+ // We only reset the ensure flag if we aren't currently waiting for a forced start.
+ // If they hit refresh while it's already booting, don't break the state machine.
+ if (!isForceStarting) {
+ ensureCalledRef.current = false;
+ setEnsureStatus("idle");
+ }
+ }, [refreshKey, isForceStarting]);
useLayoutEffect(() => {
if (!primaryRunning?.url) {
@@ -184,29 +190,26 @@ export default function PreviewTab() {
bridge.registerPreviewIframe(iframeDomRef.current, iframeSrc);
}, [bridge, iframeSrc]);
- const [isForceStarting, setIsForceStarting] = useState(false);
-
// Determine which empty state to show.
const emptyContent = (() => {
if (loading && !anatomy) return ;
if (inFlightApp) return ;
if (failedApp) return ;
+
// Dev server is in the process of booting (either picked up from anatomy
// or we just fired the ensure endpoint and are waiting for the DB row).
- if (primaryStarting) {
+ // If isForceStarting is true, we know we clicked the manual start button
+ // and are waiting for the DB to reflect 'starting'.
+ if (primaryStarting || ensureStatus === "starting" || isForceStarting) {
return (
);
}
- if (
- ensureStatus === "calling" ||
- ensureStatus === "starting" ||
- isForceStarting
- ) {
+ if (ensureStatus === "calling") {
return (
setIsForceStarting(false));
+ )
+ .then((res) => {
+ if (!res.ok) throw new Error("Failed to start");
+ })
+ .catch(() => setIsForceStarting(false));
}}
/>
);