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, iframeSrc]);
const [isForceStarting, setIsForceStarting] = useState(false);
// Determine which empty state to show.
const emptyContent = (() => {
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 (
<WarmingUpState
startedAt={undefined}
@@ -200,7 +206,19 @@ export default function PreviewTab() {
);
}
// 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 (
@@ -421,7 +439,7 @@ function FailedState({
);
}
function NotRunningState() {
function NotRunningState({ onStart }: { onStart: () => void }) {
return (
<div style={{ textAlign: "center", maxWidth: 260 }}>
<div
@@ -443,9 +461,25 @@ function NotRunningState() {
</div>
<p style={emptyTitle}>Preview not running</p>
<p style={emptySubtext}>No dev server found on port 3000.</p>
<p style={{ ...emptySubtext, marginTop: 4 }}>
Ask the AI to start the dev server.
</p>
<button
onClick={onStart}
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>
);
}

View File

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