diff --git a/frontend/src/app/pages/Dashboard/hooks/interaction/useCanvasControls.ts b/frontend/src/app/pages/Dashboard/hooks/interaction/useCanvasControls.ts index 9a1d4cd1..1b1071f8 100644 --- a/frontend/src/app/pages/Dashboard/hooks/interaction/useCanvasControls.ts +++ b/frontend/src/app/pages/Dashboard/hooks/interaction/useCanvasControls.ts @@ -584,6 +584,7 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?: cardRects: Array<{ x: number; y: number; width: number; height: number }>, maxZoom?: number, minZoom?: number, + centered?: boolean, ): { panX: number; panY: number; zoom: number } | null => { const viewport = viewportRef.current; if (!viewport || cardRects.length === 0) return null; @@ -615,7 +616,8 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?: ); const targetPanX = (vRect.width - contentWidth * targetZoom) / 2 - minX * targetZoom; - const topBiased = cardRects.length === 1; + // A single card normally top-biases (header up top, no dead space below). On creation we want the opposite: the new card dead-centered "in front of you", so `centered` forces true vertical centering. + const topBiased = cardRects.length === 1 && !centered; const targetPanY = topBiased ? FIT_PADDING * 0.4 - minY * targetZoom : (vRect.height - contentHeight * targetZoom) / 2 - @@ -631,10 +633,11 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?: maxZoom?: number, animate?: boolean, minZoom?: number, + centered?: boolean, ) => { cancelAnimation(); - const target = computeFitTarget(cardRects, maxZoom, minZoom); + const target = computeFitTarget(cardRects, maxZoom, minZoom, centered); if (!target) { // Keep current camera; snapping to (0,0,1) used to desync the minimap. if (cardRects.length === 0 || !viewportRef.current) { @@ -652,7 +655,7 @@ export function useCanvasControls(zoomSensitivity: number = 50, contentBounds?: // Settle pass: cancelAnimation() must be able to cancel it, else back-to-back fitToCards races and the first settle overwrites the second target. settleTimerRef.current = window.setTimeout(() => { settleTimerRef.current = null; - const fresh = computeFitTarget(cardRects, maxZoom, minZoom); + const fresh = computeFitTarget(cardRects, maxZoom, minZoom, centered); if (!fresh) return; const cur2 = stateRef.current; const drift = diff --git a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts index 74458766..08d71662 100644 --- a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts +++ b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useAgentSpawn.ts @@ -25,14 +25,18 @@ import { import { generateDashboardName } from '@/shared/state/dashboardsSlice'; import type { ContextPath } from '@/app/components/editor/DirectoryBrowser'; import type { CanvasActions } from '../interaction/useCanvasControls'; +import type { useDashboardSelection } from '../state/useDashboardSelection'; +import { useSpawnPlacement } from './useSpawnPlacement'; type SpawnOrigin = { x: number; y: number; type?: 'branch' }; +type Selection = ReturnType; interface UseAgentSpawnArgs { cards: Record; expandedSessionIds: string[]; dashboardId: string; expandNewChats: boolean; + selection: Selection; canvasActions: CanvasActions; viewportRef: RefObject; toolbarRef: RefObject; @@ -52,6 +56,7 @@ export function useAgentSpawn({ expandedSessionIds, dashboardId, expandNewChats, + selection, canvasActions, viewportRef, toolbarRef, @@ -65,6 +70,7 @@ export function useAgentSpawn({ onWelcomeNewAgent, }: UseAgentSpawnArgs) { const dispatch = useAppDispatch(); + const getSpawnPlacement = useSpawnPlacement({ selection, viewportRef, canvasStateRef, expandedSessionIds }); const handleBranchFromCard = useCallback( (sourceSessionId: string, newSessionId: string) => { @@ -138,6 +144,8 @@ export function useAgentSpawn({ // Toolbar position in canvas coords drives the spawn-from-toolbar grow animation. let origin: SpawnOrigin | null = null; + // Where the chat lands: beside the selected card, else in front of the viewport. Feeds the optimistic placement below, replacing the legacy toolbar-position anchor. Center on the height it will RENDER at (expanded chats are tall) so it lands vertically centered, not high-biased. + const spawnPos = getSpawnPlacement(DEFAULT_CARD_W, expandNewChats ? EXPANDED_CARD_MIN_H : DEFAULT_CARD_H); const toolbarEl = toolbarRef.current; const vpEl = viewportRef.current; if (toolbarEl && vpEl) { @@ -165,9 +173,10 @@ export function useAgentSpawn({ if (origin) spawnOriginsRef.current![draftId] = origin; const cardHeight = expandNewChats ? EXPANDED_CARD_MIN_H : DEFAULT_CARD_H; - const anchorX = browserAnchor ? browserAnchor.x - DEFAULT_CARD_W - GRID_GAP * 12 : origin?.x ?? 0; - const anchorY = browserAnchor ? browserAnchor.y : origin?.y ?? 0; - dispatch(placeCard({ sessionId: draftId, x: anchorX, y: anchorY, width: DEFAULT_CARD_W, height: cardHeight, expandedSessionIds })); + const anchorX = browserAnchor ? browserAnchor.x - DEFAULT_CARD_W - GRID_GAP * 12 : spawnPos.x; + const anchorY = browserAnchor ? browserAnchor.y : spawnPos.y; + // exact pins the resolved spawn point for the viewport-center / beside-card cases (dead-center, overlap allowed); the single-browser dock omits it so placeCard's collision-dodge cascades the chat off an occupied slot. + dispatch(placeCard({ sessionId: draftId, x: anchorX, y: anchorY, width: DEFAULT_CARD_W, height: cardHeight, expandedSessionIds, exact: !browserAnchor })); if (expandNewChats) { dispatch(expandSession(draftId)); setAutoFocusSessionId(draftId); @@ -189,7 +198,7 @@ export function useAgentSpawn({ if (bc) rects.push({ x: bc.x, y: bc.y, width: bc.width, height: bc.height }); } } - canvasActions.fitToCards(rects, 1.15, true); + canvasActions.fitToCards(rects, 1.15, true, undefined, true); handleHighlightCard(draftId); } @@ -212,7 +221,7 @@ export function useAgentSpawn({ if (launchAndSendFirstMessage.fulfilled.match(action)) { const realId = action.payload.session.id; dispatch(generateTitle({ sessionId: realId, prompt })); - // Re-point focus/selection at the rekeyed real card; the draft id is gone after the in-place swap. The browser tether rekeys with the card in the dashboardLayout extraReducer. + // Re-point focus/selection at the rekeyed real card; the draft id is gone after the in-place swap. The browser tether rekeys with the card in the dashboardLayout extraReducer. Placement + centered fit already happened optimistically at spawn (spawnPos / browserAnchor), so there's nothing to re-place here. if (expandNewChats) setAutoFocusSessionId(realId); else setPendingSelectSessionId(realId); @@ -240,7 +249,7 @@ export function useAgentSpawn({ } }); }, - [viewportRef, canvasActions, dispatch, dashboardId, expandNewChats, expandedSessionIds, handleHighlightCard, setToolbarOpen, setAutoFocusSessionId, setPendingSelectSessionId, toolbarRef, canvasStateRef, spawnOriginsRef], + [viewportRef, canvasActions, dispatch, dashboardId, expandNewChats, expandedSessionIds, getSpawnPlacement, handleHighlightCard, setToolbarOpen, setAutoFocusSessionId, setPendingSelectSessionId, toolbarRef, canvasStateRef, spawnOriginsRef], ); return { diff --git a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardCardActions.ts b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardCardActions.ts index 7b095152..89d743e4 100644 --- a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardCardActions.ts +++ b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardCardActions.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, type Dispatch, type SetStateAction } from 'react'; +import { useCallback, useEffect, type Dispatch, type RefObject, type SetStateAction } from 'react'; import { report } from '@/shared/serviceClient'; import { store } from '@/shared/state/store'; import { useAppDispatch } from '@/shared/hooks'; @@ -9,10 +9,17 @@ import { addBrowserCard, addNote, clearPendingFocusNoteId, + DEFAULT_VIEW_CARD_W, + DEFAULT_VIEW_CARD_H, + DEFAULT_BROWSER_CARD_W, + DEFAULT_BROWSER_CARD_H, + DEFAULT_NOTE_W, + DEFAULT_NOTE_H, EXPANDED_CARD_MIN_H, } from '@/shared/state/dashboardLayoutSlice'; import type { CardType, useDashboardSelection } from '../state/useDashboardSelection'; import type { CanvasActions } from '../interaction/useCanvasControls'; +import { useSpawnPlacement } from './useSpawnPlacement'; type Selection = ReturnType; @@ -23,6 +30,8 @@ interface UseDashboardCardActionsArgs { selection: Selection; canvasActions: CanvasActions; getCardRect: (id: string, type: CardType) => { x: number; y: number; width: number; height: number } | undefined; + viewportRef: RefObject; + canvasStateRef: RefObject<{ panX: number; panY: number; zoom: number }>; handleHighlightCard: (cardId: string) => void; setAutoFocusSessionId: Dispatch>; } @@ -34,13 +43,17 @@ export function useDashboardCardActions({ selection, canvasActions, getCardRect, + viewportRef, + canvasStateRef, handleHighlightCard, setAutoFocusSessionId, }: UseDashboardCardActionsArgs) { const dispatch = useAppDispatch(); + const getSpawnPlacement = useSpawnPlacement({ selection, viewportRef, canvasStateRef, expandedSessionIds }); const handleAddView = useCallback((outputId: string, opts?: { newInstance?: boolean }) => { - dispatch(addViewCard({ outputId, expandedSessionIds, newInstance: opts?.newInstance })); + const pos = getSpawnPlacement(DEFAULT_VIEW_CARD_W, DEFAULT_VIEW_CARD_H); + dispatch(addViewCard({ outputId, expandedSessionIds, x: pos.x, y: pos.y, newInstance: opts?.newInstance })); setTimeout(() => { // Focus whichever card the dispatch produced: with newInstance that's the highest-numbered instance of this output, else the primary. const viewCards = store.getState().dashboardLayout.viewCards; @@ -52,41 +65,34 @@ export function useDashboardCardActions({ } const card = viewCards[focusKey]; if (card) { - canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], 1.15, true); + canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], 1.15, true, undefined, true); handleHighlightCard(focusKey); } }, 200); - }, [dispatch, expandedSessionIds, canvasActions, handleHighlightCard]); + }, [dispatch, expandedSessionIds, getSpawnPlacement, canvasActions, handleHighlightCard]); const handleAddBrowser = useCallback(() => { report('dashboard', 'browser_added'); - const prevIds = new Set(Object.keys(store.getState().dashboardLayout.browserCards)); - dispatch(addBrowserCard({ url: browserHomepage, expandedSessionIds })); - setTimeout(() => { - const allBrowserCards = store.getState().dashboardLayout.browserCards; - const newId = Object.keys(allBrowserCards).find((id) => !prevIds.has(id)); - if (newId) { - const card = allBrowserCards[newId]; - canvasActions.fitToCards([{ x: card.x, y: card.y, width: card.width, height: card.height }], 1.15, true); - handleHighlightCard(newId); - } - }, 200); - }, [dispatch, browserHomepage, expandedSessionIds, canvasActions, handleHighlightCard]); + // Camera focus + highlight are handled by the pendingFocusBrowserId effect (useDashboardLifecycle), which fires for browsers from every path (toolbar, link clicks). Doing it here too would double-fit and fight that effect's zoom. + const pos = getSpawnPlacement(DEFAULT_BROWSER_CARD_W, DEFAULT_BROWSER_CARD_H); + dispatch(addBrowserCard({ url: browserHomepage, expandedSessionIds, x: pos.x, y: pos.y })); + }, [dispatch, browserHomepage, expandedSessionIds, getSpawnPlacement]); const handleAddNote = useCallback(() => { report('dashboard', 'note_added'); const prevIds = new Set(Object.keys(store.getState().dashboardLayout.notes)); - dispatch(addNote({ expandedSessionIds })); + const pos = getSpawnPlacement(DEFAULT_NOTE_W, DEFAULT_NOTE_H); + dispatch(addNote({ expandedSessionIds, x: pos.x, y: pos.y })); setTimeout(() => { const allNotes = store.getState().dashboardLayout.notes; const newId = Object.keys(allNotes).find((id) => !prevIds.has(id)); if (newId) { const note = allNotes[newId]; - canvasActions.fitToCards([{ x: note.x, y: note.y, width: note.width, height: note.height }], 1.15, true); + canvasActions.fitToCards([{ x: note.x, y: note.y, width: note.width, height: note.height }], 1.15, true, undefined, true); handleHighlightCard(newId); } }, 200); - }, [dispatch, expandedSessionIds, canvasActions, handleHighlightCard]); + }, [dispatch, expandedSessionIds, getSpawnPlacement, canvasActions, handleHighlightCard]); // Auto-clear pendingFocusNoteId after the note has had a chance to mount + autofocus. useEffect(() => { diff --git a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts index 90e79f6c..40472bcd 100644 --- a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts +++ b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useDashboardLifecycle.ts @@ -236,6 +236,7 @@ export function useDashboardLifecycle({ 1.15, true, 0.8, + true, ); handleHighlightCard(browserId); } diff --git a/frontend/src/app/pages/Dashboard/hooks/lifecycle/useSpawnPlacement.ts b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useSpawnPlacement.ts new file mode 100644 index 00000000..9779a511 --- /dev/null +++ b/frontend/src/app/pages/Dashboard/hooks/lifecycle/useSpawnPlacement.ts @@ -0,0 +1,40 @@ +import { useCallback, type RefObject } from 'react'; +import { store } from '@/shared/state/store'; +import { computeSpawnPosition } from '@/shared/state/dashboardLayoutSlice'; +import { getCardRect } from '../../geometry/getCardRect'; +import type { useDashboardSelection } from '../state/useDashboardSelection'; + +type Selection = ReturnType; + +interface Args { + selection: Selection; + viewportRef: RefObject; + canvasStateRef: RefObject<{ panX: number; panY: number; zoom: number }>; + expandedSessionIds: string[]; +} + +// Resolves where a freshly created card (chat/app/browser/note) should land: docked beside the selected card if one is selected, otherwise centered in the current viewport ("in front of you"). Both paths collision-dodge inside computeSpawnPosition. Returns a getter that reads selection + the live store at call time, so the answer reflects whatever is selected the moment the user hits create. +export function useSpawnPlacement({ selection, viewportRef, canvasStateRef, expandedSessionIds }: Args) { + return useCallback( + (width: number, height: number): { x: number; y: number } => { + const layout = store.getState().dashboardLayout; + // Most-recently selected card wins as the dock anchor (Map preserves insertion order). + const selected = Array.from(selection.selectedIds.entries()); + if (selected.length > 0) { + const [id, type] = selected[selected.length - 1]; + const rect = getCardRect(id, type); + if (rect) return computeSpawnPosition(layout, width, height, { beside: rect }, expandedSessionIds); + } + const vp = viewportRef.current; + const cs = canvasStateRef.current; + if (vp && cs) { + const vr = vp.getBoundingClientRect(); + const cx = (vr.width / 2 - cs.panX) / cs.zoom; + const cy = (vr.height / 2 - cs.panY) / cs.zoom; + return computeSpawnPosition(layout, width, height, { viewportCenter: { x: cx, y: cy } }, expandedSessionIds); + } + return computeSpawnPosition(layout, width, height, {}, expandedSessionIds); + }, + [selection, viewportRef, canvasStateRef, expandedSessionIds], + ); +} diff --git a/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts b/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts index fff150d7..f86f74a3 100644 --- a/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts +++ b/frontend/src/app/pages/Dashboard/hooks/state/useDashboardController.ts @@ -247,6 +247,7 @@ export function useDashboardController(dashboardId: string, isActive: boolean) { expandedSessionIds, dashboardId, expandNewChats, + selection, canvasActions: canvas.actions, viewportRef: canvas.viewportRef, toolbarRef, @@ -274,6 +275,8 @@ export function useDashboardController(dashboardId: string, isActive: boolean) { selection, canvasActions: canvas.actions, getCardRect, + viewportRef: canvas.viewportRef, + canvasStateRef, handleHighlightCard, setAutoFocusSessionId, }); diff --git a/frontend/src/shared/state/dashboardLayoutSlice.ts b/frontend/src/shared/state/dashboardLayoutSlice.ts index 9720b790..18ff1735 100644 --- a/frontend/src/shared/state/dashboardLayoutSlice.ts +++ b/frontend/src/shared/state/dashboardLayoutSlice.ts @@ -441,6 +441,25 @@ export function placeBesideCard( return findOpenSpotNear(targetX, targetY, rects, newW, newH); } +// Dock a chat-spawned browser to the right of its chat card. Unlike placeBesideCard, this ALWAYS lands beside the chat (overlap is fine, the new card sits on top via zOrder) so an occupied spot can never fling the browser to a far grid cell or stack it under an unrelated card. Only this chat's OWN browsers (same spawned_by, still in the column) stack under each other so siblings don't fully cover one another; every other card is ignored. +export function placeBrowserBesideChat( + state: DashboardLayoutState, + chat: { x: number; y: number; width: number; height: number }, + parentSessionId: string, + newW: number, + newH: number, + excludeBrowserId?: string, +): { x: number; y: number } { + const targetX = chat.x + chat.width + GRID_GAP * 12; + const siblings = Object.values(state.browserCards).filter( + (c) => c.browser_id !== excludeBrowserId && c.spawned_by === parentSessionId && Math.abs(c.x - targetX) < 50, + ); + const targetY = siblings.length > 0 + ? Math.max(...siblings.map((c) => c.y + c.height)) + GRID_GAP + : chat.y; + return { x: targetX, y: targetY }; +} + // Dock a new card directly below an anchor card (left edges aligned). Used for a browser spawned by a Workflows-hub chat, which has no agent card to sit beside. export function placeBelowCard( state: DashboardLayoutState, @@ -469,6 +488,29 @@ export function placeInParentColumn( return placeBesideCard(state, parentCard, newW, newH, expandedSessionIds, exclude); } +// Where a user-created card (chat/app/browser/note) should land. Resolved in the UI layer where selection + viewport are known, then handed to the add reducers as an explicit x/y. `beside` (the currently selected card) docks the new card to its right, stacking under that column (collision-aware); `viewportCenter` (canvas-space center of what the user is looking at) drops it dead-center "in front of you", overlapping whatever's there. With neither, falls back to the legacy top-left grid scan. +export interface SpawnAnchor { + beside?: { x: number; y: number; width: number; height: number }; + viewportCenter?: { x: number; y: number }; +} + +export function computeSpawnPosition( + state: DashboardLayoutState, + newW: number, + newH: number, + anchor: SpawnAnchor, + expandedSessionIds?: string[], +): { x: number; y: number } { + if (anchor.beside) { + return placeBesideCard(state, anchor.beside, newW, newH, expandedSessionIds); + } + if (anchor.viewportCenter) { + // Land dead-center, "in front of you", even if a card is already there. Overlap is intentional (new card sits on top via its higher zOrder); dodging to free space is exactly the "spawned off to the side" behavior we're removing. + return { x: anchor.viewportCenter.x - newW / 2, y: anchor.viewportCenter.y - newH / 2 }; + } + return findOpenGridCell(collectOccupiedRects(state, expandedSessionIds), newW, newH); +} + // Reconnect-refetch merge: ADD only the cards the snapshot carries that the client is missing (e.g. a spawned browser whose broadcast was lost in a socket gap), collision-resolving each against the live layout so a recovered card can't land on a card already on canvas, and NEVER touch a card the client already has (that's exactly what preserves its live, collision-placed position). The shared `occupied` list carries placements forward so two recovered cards in the same pass also avoid each other. function addMissingCards( live: Record, @@ -522,11 +564,12 @@ const dashboardLayoutSlice = createSlice({ height: number; // Optional: which existing sessions are currently expanded (showing their full chat history). Without this, the collision check uses each card's STORED height, which is the collapsed value, even when the card is currently rendering at the expanded ~620px. Result: new sub-agent cards spawn into the collapsed footprint but overlap the visually expanded one. Caller (Dashboard.tsx) passes the current expanded set so the collision math matches what the user actually sees. expandedSessionIds?: string[]; + // Honor the given x/y verbatim (dead-center "in front of you", overlap allowed) instead of collision-dodging to a free grid cell. Set when the caller already resolved the spot (viewport center / beside a selected card) and dodging would defeat that; tidyLayout cleans up any overlap on demand. + exact?: boolean; }> ) { - const { sessionId, x, y, width, height, expandedSessionIds } = action.payload; - const rects = collectOccupiedRects(state, expandedSessionIds); - const pos = findOpenSpotNear(x, y, rects, width, height); + const { sessionId, x, y, width, height, expandedSessionIds, exact } = action.payload; + const pos = exact ? { x, y } : findOpenSpotNear(x, y, collectOccupiedRects(state, expandedSessionIds), width, height); state.cards[sessionId] = { session_id: sessionId, x: pos.x, @@ -766,11 +809,13 @@ const dashboardLayoutSlice = createSlice({ state.activeViewCardId = action.payload; }, - addBrowserCard(state, action: PayloadAction<{ url: string; expandedSessionIds?: string[] }>) { + addBrowserCard(state, action: PayloadAction<{ url: string; expandedSessionIds?: string[]; x?: number; y?: number }>) { const id = `browser-${Date.now().toString(36)}`; const tabId = generateTabId(); - const rects = collectOccupiedRects(state, action.payload.expandedSessionIds); - const pos = findOpenGridCell(rects, DEFAULT_BROWSER_CARD_W, DEFAULT_BROWSER_CARD_H); + // Caller may pre-resolve the spawn position (beside the selected card, or in front of the viewport); otherwise fall back to the top-left grid scan. + const pos = action.payload.x != null && action.payload.y != null + ? { x: action.payload.x, y: action.payload.y } + : findOpenGridCell(collectOccupiedRects(state, action.payload.expandedSessionIds), DEFAULT_BROWSER_CARD_W, DEFAULT_BROWSER_CARD_H); state.browserCards[id] = { browser_id: id, url: action.payload.url, @@ -1626,6 +1671,17 @@ const dashboardLayoutSlice = createSlice({ for (const entry of Object.values(state.glowingBrowserCards)) { if (entry.sourceId === draftId) entry.sourceId = session.id; } + // First-turn browser race: a browser the first message spawns carries parent_session_id = the real id, so its browser_card_added can land BEFORE this re-key, find no parent card, and fall back to the grid. Now that the chat card exists under the real id, dock each such browser beside it (freshly spawned, so not user-moved yet) and restore the tether the racing path skipped. + const parentCard = state.cards[session.id]; + if (parentCard) { + for (const bc of Object.values(state.browserCards)) { + if (bc.spawned_by !== session.id) continue; + const pos = placeBrowserBesideChat(state, parentCard, session.id, bc.width, bc.height, bc.browser_id); + bc.x = pos.x; + bc.y = pos.y; + state.glowingBrowserCards[bc.browser_id] = { sourceId: session.id, fading: false, label: 'Use Browser' }; + } + } }); }, }); diff --git a/frontend/src/shared/ws/WebSocketManager.ts b/frontend/src/shared/ws/WebSocketManager.ts index 24c0a591..e360ef50 100644 --- a/frontend/src/shared/ws/WebSocketManager.ts +++ b/frontend/src/shared/ws/WebSocketManager.ts @@ -28,7 +28,7 @@ import { clearTurnLabel, } from '../state/agentsSlice'; import { streamStart, streamDelta, streamEnd, clearStreamingForSession } from '../state/streamingSlice'; -import { addBrowserCardFromBackend, markBrowserCardEnding, keepBrowserCardOpen, placeBesideCard, placeBelowCard, setBrowserCardPosition, setGlowingBrowserCards, fadeGlowingBrowserCards, clearGlowingBrowserCards, GRID_GAP, WORKFLOW_CARD_GAP, openWorkflowsApp, openWorkflowMonitor } from '../state/dashboardLayoutSlice'; +import { addBrowserCardFromBackend, markBrowserCardEnding, keepBrowserCardOpen, placeBesideCard, placeBelowCard, placeBrowserBesideChat, setBrowserCardPosition, setGlowingBrowserCards, fadeGlowingBrowserCards, clearGlowingBrowserCards, GRID_GAP, WORKFLOW_CARD_GAP, openWorkflowsApp, openWorkflowMonitor } from '../state/dashboardLayoutSlice'; import { upsertOutput } from '../state/outputsSlice'; import { fetchSettings } from '../state/settingsSlice'; import { displaySessionName } from '../state/sessionDisplay'; @@ -810,7 +810,7 @@ class WebSocketManager { let pos: { x: number; y: number } | null = null; let glowLabel = 'Use Browser'; if (parentCard) { - pos = placeBesideCard(layoutState, parentCard, browserCard.width, browserCard.height, undefined, exclude); + pos = placeBrowserBesideChat(layoutState, parentCard, parentId, browserCard.width, browserCard.height, browserCard.browser_id); } else if (sess?.workflow_run_id && layoutState.workflowsMonitorCard) { pos = placeBesideCard(layoutState, layoutState.workflowsMonitorCard, browserCard.width, browserCard.height, undefined, exclude, WORKFLOW_CARD_GAP, true); } else if (sess?.workflow_edit_id && layoutState.workflowsHub) {