feat(preview): add 1-click start dev server button to empty state

This commit is contained in:
2026-06-11 17:07:17 -07:00
parent 7337e2c5b0
commit 08fbe8405b
2 changed files with 48 additions and 12 deletions

View File

@@ -174,6 +174,8 @@ export default function PreviewTab() {
bridge.registerPreviewIframe(iframeDomRef.current, iframeSrc); bridge.registerPreviewIframe(iframeDomRef.current, iframeSrc);
}, [bridge, iframeSrc]); }, [bridge, iframeSrc]);
const [isForceStarting, setIsForceStarting] = useState(false);
// Determine which empty state to show. // Determine which empty state to show.
const emptyContent = (() => { const emptyContent = (() => {
if (loading && !anatomy) return <InitialLoader />; if (loading && !anatomy) return <InitialLoader />;
@@ -190,7 +192,11 @@ export default function PreviewTab() {
/> />
); );
} }
if (ensureStatus === "calling" || ensureStatus === "starting") { if (
ensureStatus === "calling" ||
ensureStatus === "starting" ||
isForceStarting
) {
return ( return (
<WarmingUpState <WarmingUpState
startedAt={undefined} startedAt={undefined}
@@ -200,7 +206,19 @@ export default function PreviewTab() {
); );
} }
// Never had a dev server — needs the AI to start one. // Never had a dev server — needs the AI to start one.
return <NotRunningState />; return (
<NotRunningState
onStart={() => {
setIsForceStarting(true);
fetch(
`/api/projects/${projectId}/dev-server/ensure?forceStart=true`,
{
method: "POST",
},
).catch(() => setIsForceStarting(false));
}}
/>
);
})(); })();
return ( return (
@@ -421,7 +439,7 @@ function FailedState({
); );
} }
function NotRunningState() { function NotRunningState({ onStart }: { onStart: () => void }) {
return ( return (
<div style={{ textAlign: "center", maxWidth: 260 }}> <div style={{ textAlign: "center", maxWidth: 260 }}>
<div <div
@@ -443,9 +461,25 @@ function NotRunningState() {
</div> </div>
<p style={emptyTitle}>Preview not running</p> <p style={emptyTitle}>Preview not running</p>
<p style={emptySubtext}>No dev server found on port 3000.</p> <p style={emptySubtext}>No dev server found on port 3000.</p>
<p style={{ ...emptySubtext, marginTop: 4 }}> <button
Ask the AI to start the dev server. onClick={onStart}
</p> style={{
marginTop: 16,
background: "#18181b",
color: "white",
border: "none",
padding: "8px 16px",
borderRadius: 8,
fontSize: "0.8rem",
fontWeight: 500,
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
gap: 6,
}}
>
Start Dev Server
</button>
</div> </div>
); );
} }

View File

@@ -23,7 +23,7 @@ import {
} from "@/lib/dev-container"; } from "@/lib/dev-container";
export async function POST( export async function POST(
_req: Request, request: Request,
{ params }: { params: Promise<{ projectId: string }> }, { params }: { params: Promise<{ projectId: string }> },
) { ) {
const { projectId } = await params; const { projectId } = await params;
@@ -96,7 +96,10 @@ export async function POST(
[projectId], [projectId],
); );
if (!last) { const forceStart =
new URL(request.url).searchParams.get("forceStart") === "true";
if (!last && !forceStart) {
return NextResponse.json({ status: "no_history" }); return NextResponse.json({ status: "no_history" });
} }
@@ -111,13 +114,12 @@ export async function POST(
} }
// 4. Fire restart in background — don't block the response. // 4. Fire restart in background — don't block the response.
// The probe (up to 300s) runs in background; anatomy polling at 5s // If forceStart is true but we have no history, default to Next.js start command.
// will surface state='starting' immediately, then 'running' when ready.
const restartOpts = { const restartOpts = {
projectId: project.id, projectId: project.id,
projectSlug: project.slug, projectSlug: project.slug,
command: last.command, command: last?.command || "next dev -H 0.0.0.0 --no-turbopack",
port: last.port, port: last?.port || 3000,
workspace, workspace,
}; };