[aidan] ux: when agent creates app always opens in dashboard

This commit is contained in:
abccodes
2026-06-14 04:53:54 -07:00
parent 2e3ce84572
commit 6f8ad19bce
3 changed files with 55 additions and 1 deletions
@@ -38,6 +38,7 @@ interface UseAgentSpawnArgs {
setToolbarOpen: Dispatch<SetStateAction<boolean>>;
setAutoFocusSessionId: Dispatch<SetStateAction<string | null>>;
setPendingSelectSessionId: Dispatch<SetStateAction<string | null>>;
pendingViewBuilderSessionsRef: RefObject<Set<string>>;
}
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' }));
@@ -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<boolean>;
restoredExpandedRef: MutableRefObject<boolean>;
pendingViewBuilderSessionsRef: RefObject<Set<string>>;
}
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<string | null>(null);
useEffect(() => {
if (!dashboardId) return;
@@ -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<Set<string>>(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 {