From 00b44274cf77f17c96e2ab2b6bce7f32a67dd822 Mon Sep 17 00:00:00 2001 From: abccodes Date: Sun, 14 Jun 2026 05:29:27 -0700 Subject: [PATCH] [aidan] ui/ux: when building app window appears immediately --- .../Dashboard/cards/DashboardViewCard.tsx | 49 ++++++++++++++++++- .../hooks/lifecycle/useAgentSpawn.ts | 8 --- .../hooks/lifecycle/useDashboardLifecycle.ts | 48 ++++++++++-------- .../hooks/state/useDashboardController.ts | 9 ---- 4 files changed, 75 insertions(+), 39 deletions(-) diff --git a/frontend/src/app/pages/Dashboard/cards/DashboardViewCard.tsx b/frontend/src/app/pages/Dashboard/cards/DashboardViewCard.tsx index 034b4341..af3d5d2e 100644 --- a/frontend/src/app/pages/Dashboard/cards/DashboardViewCard.tsx +++ b/frontend/src/app/pages/Dashboard/cards/DashboardViewCard.tsx @@ -1,6 +1,7 @@ import React, { useState, useRef, useCallback, useEffect } from 'react'; import { createPortal } from 'react-dom'; import Box from '@mui/material/Box'; +import Fade from '@mui/material/Fade'; import Typography from '@mui/material/Typography'; import IconButton from '@mui/material/IconButton'; import Tooltip from '@mui/material/Tooltip'; @@ -10,7 +11,7 @@ import CloseIcon from '@mui/icons-material/Close'; import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded'; import { Output, SERVE_BASE } from '@/shared/state/outputsSlice'; import { setViewCardPosition, setViewCardSize, removeViewCard } from '@/shared/state/dashboardLayoutSlice'; -import { useAppDispatch } from '@/shared/hooks'; +import { useAppDispatch, useAppSelector } from '@/shared/hooks'; import { API_BASE, getAuthToken } from '@/shared/config'; import { useClaudeTokens } from '@/shared/styles/ThemeContext'; import ViewPreview, { ViewPreviewHandle } from '@/app/pages/Views/ViewPreview'; @@ -407,6 +408,7 @@ const DashboardViewCard: React.FC = ({ inputData={inputData} backendResult={backendResult} /> + {/* Resize handles */} @@ -481,6 +483,51 @@ const DashboardViewCard: React.FC = ({ export default React.memo(DashboardViewCard); +// Calm overlay shown while the App Builder chat that owns this output is +// actively editing it. Hides whatever transient half-broken state the agent +// might be writing through (a missing import, a syntax error mid-keystroke) +// so the user sees "Building..." instead of an error iframe. Fades in/out. +const BuildingOverlay: React.FC<{ sessionId: string | null }> = ({ sessionId }) => { + const c = useClaudeTokens(); + const status = useAppSelector((s) => (sessionId ? s.agents.sessions[sessionId]?.status : undefined)); + const isBuilding = status === 'running' || status === 'waiting_approval'; + return ( + + + + + Building… + + + + ); +}; + // Old-mode outputs render the legacy serve URL; new-mode webapp_template outputs attach to a runtime and point the webview at Vite once frontend_url arrives. const DashboardOutputPreview: React.FC<{ previewRef: React.Ref; diff --git a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts index 862c8728..e1ff4cbc 100644 --- a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts +++ b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts @@ -38,7 +38,6 @@ interface UseAgentSpawnArgs { setToolbarOpen: Dispatch>; setAutoFocusSessionId: Dispatch>; setPendingSelectSessionId: Dispatch>; - pendingViewBuilderSessionsRef: RefObject>; } export function useAgentSpawn({ @@ -55,7 +54,6 @@ export function useAgentSpawn({ setToolbarOpen, setAutoFocusSessionId, setPendingSelectSessionId, - pendingViewBuilderSessionsRef, }: UseAgentSpawnArgs) { const dispatch = useAppDispatch(); @@ -157,12 +155,6 @@ export function useAgentSpawn({ ).then((action) => { if (launchAndSendFirstMessage.fulfilled.match(action)) { const realId = action.payload.session.id; - // App Builder chats expect a view card to pop in next to the chat - // once the backend seeds its Output row. Register the session id - // so useDashboardLifecycle's output watcher can do the auto-open. - if (mode === 'view-builder') { - pendingViewBuilderSessionsRef.current?.add(realId); - } dispatch(generateTitle({ sessionId: realId, prompt })); if (selectedBrowserIds?.length) { dispatch(setGlowingBrowserCards({ browserIds: selectedBrowserIds, sessionId: realId, label: 'Use Browser' })); diff --git a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts index f49da09d..d7777132 100644 --- a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts +++ b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, type MutableRefObject, type RefObject } from 'react'; +import { useEffect, useRef, type MutableRefObject } from 'react'; import { report } from '@/shared/serviceClient'; import { store } from '@/shared/state/store'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; @@ -40,7 +40,6 @@ interface UseDashboardLifecycleArgs { handleHighlightCard: (cardId: string) => void; hasFittedRef: MutableRefObject; restoredExpandedRef: MutableRefObject; - pendingViewBuilderSessionsRef: RefObject>; } export function useDashboardLifecycle({ @@ -57,7 +56,6 @@ export function useDashboardLifecycle({ handleHighlightCard, hasFittedRef, restoredExpandedRef, - pendingViewBuilderSessionsRef, }: UseDashboardLifecycleArgs) { const dispatch = useAppDispatch(); const pendingBrowserUrl = useAppSelector((state) => state.tempState.pendingBrowserUrl); @@ -256,27 +254,35 @@ export function useDashboardLifecycle({ } }, [layoutInitialized, outputsLoaded, viewCards, outputs, dispatch]); - // App Builder auto-open: when a view-builder session launched on this - // dashboard, useAgentSpawn parks its id in pendingViewBuilderSessionsRef. - // The backend then seeds an Output row and broadcasts agent:output_upserted; - // when that lands in outputsSlice we drop a ViewCard next to the chat so the - // user sees the app appear without a manual "open" step. Per-mount tracked - // (not redux) so a manual close after auto-open stays closed. + // On first load after outputs settle, snapshot every existing Output id as + // "already accounted for." Any output that ARRIVES later (typically the + // agent:output_upserted WS broadcast the backend fires the instant a + // view-builder session is seeded, at session start) whose session_id points + // at a view-builder chat on this dashboard gets a view card dropped on the + // canvas right away. Per-mount tracked so a manual close after auto-open + // stays closed. Prior approach keyed off a pending-set populated inside + // launchAndSendFirstMessage.then(): the WS upsert won the race and the + // effect saw an empty set, so the card didn't pop until the session-end + // meta-sync re-broadcast. + const autoOpenedOutputsRef = useRef>(new Set()); + const outputsSnapshottedRef = useRef(false); useEffect(() => { - if (!layoutInitialized) return; - const pending = pendingViewBuilderSessionsRef.current; - if (!pending || pending.size === 0) return; + if (!layoutInitialized || !outputsLoaded) return; + if (!outputsSnapshottedRef.current) { + for (const oid of Object.keys(outputs)) autoOpenedOutputsRef.current.add(oid); + outputsSnapshottedRef.current = true; + return; + } for (const output of Object.values(outputs)) { + if (autoOpenedOutputsRef.current.has(output.id)) continue; const sid = output.session_id; - if (!sid || !pending.has(sid)) continue; - if (viewCards[output.id]) { - pending.delete(sid); - continue; - } - // Let addViewCard pick a collision-free grid cell; the auto-fit below - // pans/zooms to show the chat + new view card together regardless. + if (!sid) continue; + const sess = sessions[sid]; + if (!sess || sess.mode !== 'view-builder') continue; + if (sess.dashboard_id !== dashboardId) continue; + autoOpenedOutputsRef.current.add(output.id); + if (viewCards[output.id]) continue; dispatch(addViewCard({ outputId: output.id, expandedSessionIds })); - pending.delete(sid); const outputId = output.id; setTimeout(() => { const vc = store.getState().dashboardLayout.viewCards[outputId]; @@ -288,7 +294,7 @@ export function useDashboardLifecycle({ handleHighlightCard(outputId); }, 200); } - }, [layoutInitialized, outputs, viewCards, expandedSessionIds, dispatch, canvasActions, handleHighlightCard, pendingViewBuilderSessionsRef]); + }, [layoutInitialized, outputsLoaded, outputs, sessions, viewCards, dashboardId, expandedSessionIds, dispatch, canvasActions, handleHighlightCard]); const namedOnFirstMessageRef = useRef(null); useEffect(() => { diff --git a/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts b/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts index ae367017..8b9326d6 100644 --- a/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts +++ b/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts @@ -113,13 +113,6 @@ export function useDashboardController(dashboardId: string, isActive: boolean) { contentRef: canvas.contentRef, }); - // Session ids that just launched in App Builder ('view-builder') mode and - // are still awaiting their backend-seeded Output row. useAgentSpawn writes - // here; useDashboardLifecycle consumes it to auto-open the view card the - // moment the output upsert arrives. Per-mount ref (not redux) so a manual - // close after auto-open stays closed. - const pendingViewBuilderSessionsRef = useRef>(new Set()); - useDashboardLifecycle({ isActive, dashboardId, @@ -134,7 +127,6 @@ export function useDashboardController(dashboardId: string, isActive: boolean) { handleHighlightCard, hasFittedRef, restoredExpandedRef, - pendingViewBuilderSessionsRef, }); // ---- Auto-reveal / collapse / unreveal sub-agent cards ---- @@ -229,7 +221,6 @@ export function useDashboardController(dashboardId: string, isActive: boolean) { setToolbarOpen, setAutoFocusSessionId, setPendingSelectSessionId, - pendingViewBuilderSessionsRef, }); const {