From bb4b4df987cfca34e67d50db905ee54ead419e46 Mon Sep 17 00:00:00 2001 From: mawkone Date: Wed, 10 Jun 2026 21:40:48 -0700 Subject: [PATCH] fix(stop+stability): stop button interrupts live generation; classifier, prompt + preview pane improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stop button fix: - Plumb AbortSignal end-to-end: callVibnChat → Gemini SDK (config.abortSignal) / OpenAI fetch → executeMcpTool (/api/mcp fetch) - Treat abort as clean user stop (not fatal error); partial reply persisted with '(stopped by user)' Classifier fix: - Add timeout/gateway/5xx/connection-error vocabulary to diagnose intent - Prevents 'I get a gateway timeout' from falling through to feature_build (40 rounds) and looping Prompt / agent behaviour: - Render verification is now scope-aware: small edits stop at green healthCheck; no browser_console/curl audit on healthy server - Sanitize stale '### Phase Checkpoint' walls from loaded history so old threads stop biasing new turns - Next.js dev command updated to --no-turbopack for container stability (per-route lazy compile caused cold-start 503s) - New public page prompt: agent checks middleware allowlist in the same turn - Scope discipline and QA-tool gating carried forward from prior session Code cleanup: - Remove duplicate AgentPhase declaration (TS2440) - Remove dead checkpoint emit branch and orphan 'checkpoint' phase value - Remove unused MAX_TOOL_ROUNDS constant Preview pane (build status): - 4-state machine: initial-load / building (with elapsed timer) / build-failed / not-running - pollMs 0 → 5 000ms so dev-server recovery and build completion auto-update without refresh - anatomy route + use-anatomy type: inFlightBuild gains createdAt for elapsed timer --- .../[projectId]/(home)/preview/page.tsx | 233 ++++++++++- app/api/chat/route.ts | 153 ++++---- app/api/projects/[projectId]/anatomy/route.ts | 367 +++++++++++++----- components/project/use-anatomy.ts | 14 +- components/vibn-chat/chat-panel.tsx | 116 +----- lib/ai/gemini-chat.ts | 13 +- lib/ai/openai-compatible-chat.ts | 9 +- lib/ai/vibn-chat-model.ts | 12 +- lib/ai/vibn-tools.ts | 30 +- 9 files changed, 618 insertions(+), 329 deletions(-) diff --git a/app/[workspace]/project/[projectId]/(home)/preview/page.tsx b/app/[workspace]/project/[projectId]/(home)/preview/page.tsx index c5e88069..78e9deac 100644 --- a/app/[workspace]/project/[projectId]/(home)/preview/page.tsx +++ b/app/[workspace]/project/[projectId]/(home)/preview/page.tsx @@ -19,15 +19,47 @@ function sandboxIframe(src: string, origin: string): boolean { } } +/** How long a deployment has been running, formatted as "1m 23s" */ +function useElapsed(sinceIso: string | undefined) { + const [elapsed, setElapsed] = useState(""); + useEffect(() => { + // No ISO timestamp — nothing to tick. The caller won't render + // the elapsed string when sinceIso is undefined anyway. + if (!sinceIso) return; + const update = () => { + const ms = Date.now() - new Date(sinceIso).getTime(); + if (ms < 0) return; + const s = Math.floor(ms / 1000); + const m = Math.floor(s / 60); + setElapsed(m > 0 ? `${m}m ${s % 60}s` : `${s}s`); + }; + update(); + const id = setInterval(update, 1000); + return () => { + clearInterval(id); + setElapsed(""); + }; + }, [sinceIso]); + return elapsed; +} + export default function PreviewTab() { const params = useParams(); const projectId = params.projectId as string; - const { anatomy, loading } = useAnatomy(projectId, { pollMs: 0 }); + + // Poll every 5 s so build-state transitions surface without a manual refresh. + const { anatomy, loading } = useAnatomy(projectId, { pollMs: 5000 }); const previews = anatomy?.hosting.previews ?? []; - // Find the port 3000 preview if it exists, otherwise fall back to null const primaryPreview = previews.find((p) => p.port === 3000); + // Derive in-flight / recently-failed build from prod apps. + const liveApps = anatomy?.hosting.live ?? []; + const inFlightApp = liveApps.find((a) => a.inFlightBuild); + const failedApp = !inFlightApp + ? liveApps.find((a) => a.lastBuild?.status === "failed") + : undefined; + const [iframeSrc, setIframeSrc] = useState(null); const iframeDomRef = useRef(null); const bridge = usePreviewBridge(); @@ -45,6 +77,14 @@ export default function PreviewTab() { bridge.registerPreviewIframe(iframeDomRef.current, iframeSrc); }, [bridge, iframeSrc]); + // Derive content for the empty state. + const emptyContent = (() => { + if (loading && !anatomy) return ; + if (inFlightApp) return ; + if (failedApp) return ; + return ; + })(); + return (
{deviceMode === "mobile" && } - {loading && !iframeSrc ? ( -
- -
- ) : iframeSrc ? ( + {iframeSrc ? (