From 6f8ad19bcecb84edd9b41a7709c1da60a44dcc42 Mon Sep 17 00:00:00 2001 From: abccodes Date: Sun, 14 Jun 2026 04:53:54 -0700 Subject: [PATCH] [aidan] ux: when agent creates app always opens in dashboard --- .../hooks/lifecycle/useAgentSpawn.ts | 8 ++++ .../hooks/lifecycle/useDashboardLifecycle.ts | 39 ++++++++++++++++++- .../hooks/state/useDashboardController.ts | 9 +++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts index e1ff4cbc..862c8728 100644 --- a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts +++ b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts @@ -38,6 +38,7 @@ interface UseAgentSpawnArgs { setToolbarOpen: Dispatch>; setAutoFocusSessionId: Dispatch>; setPendingSelectSessionId: Dispatch>; + pendingViewBuilderSessionsRef: RefObject>; } export function useAgentSpawn({ @@ -54,6 +55,7 @@ export function useAgentSpawn({ setToolbarOpen, setAutoFocusSessionId, setPendingSelectSessionId, + pendingViewBuilderSessionsRef, }: UseAgentSpawnArgs) { const dispatch = useAppDispatch(); @@ -155,6 +157,12 @@ 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 cfc3a428..f49da09d 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 } from 'react'; +import { useEffect, useRef, type MutableRefObject, type RefObject } from 'react'; import { report } from '@/shared/serviceClient'; import { store } from '@/shared/state/store'; import { useAppDispatch, useAppSelector } from '@/shared/hooks'; @@ -12,6 +12,7 @@ import { fetchLayout, reconcileSessions, addBrowserCard, + addViewCard, resetLayout, removeViewCard, clearPendingFocusBrowserId, @@ -39,6 +40,7 @@ interface UseDashboardLifecycleArgs { handleHighlightCard: (cardId: string) => void; hasFittedRef: MutableRefObject; restoredExpandedRef: MutableRefObject; + pendingViewBuilderSessionsRef: RefObject>; } export function useDashboardLifecycle({ @@ -55,6 +57,7 @@ export function useDashboardLifecycle({ handleHighlightCard, hasFittedRef, restoredExpandedRef, + pendingViewBuilderSessionsRef, }: UseDashboardLifecycleArgs) { const dispatch = useAppDispatch(); const pendingBrowserUrl = useAppSelector((state) => state.tempState.pendingBrowserUrl); @@ -253,6 +256,40 @@ 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. + useEffect(() => { + if (!layoutInitialized) return; + const pending = pendingViewBuilderSessionsRef.current; + if (!pending || pending.size === 0) return; + for (const output of Object.values(outputs)) { + 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. + dispatch(addViewCard({ outputId: output.id, expandedSessionIds })); + pending.delete(sid); + const outputId = output.id; + setTimeout(() => { + const vc = store.getState().dashboardLayout.viewCards[outputId]; + if (!vc) return; + const rects = [{ x: vc.x, y: vc.y, width: vc.width, height: vc.height }]; + const ac = store.getState().dashboardLayout.cards[sid]; + if (ac) rects.push({ x: ac.x, y: ac.y, width: ac.width, height: ac.height }); + canvasActions.fitToCards(rects, 1.15, true); + handleHighlightCard(outputId); + }, 200); + } + }, [layoutInitialized, outputs, viewCards, expandedSessionIds, dispatch, canvasActions, handleHighlightCard, pendingViewBuilderSessionsRef]); + const namedOnFirstMessageRef = useRef(null); useEffect(() => { if (!dashboardId) return; diff --git a/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts b/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts index a252b71b..efd07117 100644 --- a/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts +++ b/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts @@ -113,6 +113,13 @@ 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, @@ -127,6 +134,7 @@ export function useDashboardController(dashboardId: string, isActive: boolean) { handleHighlightCard, hasFittedRef, restoredExpandedRef, + pendingViewBuilderSessionsRef, }); // ---- Auto-reveal / collapse / unreveal sub-agent cards ---- @@ -221,6 +229,7 @@ export function useDashboardController(dashboardId: string, isActive: boolean) { setToolbarOpen, setAutoFocusSessionId, setPendingSelectSessionId, + pendingViewBuilderSessionsRef, }); const {