From d1713f901c7cf0895ad4ef059237e05bdcfbd817 Mon Sep 17 00:00:00 2001 From: SirKentut <81878031+SirKentut@users.noreply.github.com> Date: Tue, 7 Jul 2026 18:14:17 -0700 Subject: [PATCH] [pierre] feat: browser spawned from agent will always spawn to the right of parent chat --- frontend/src/shared/state/dashboardLayoutSlice.ts | 14 ++++++++++++++ frontend/src/shared/ws/WebSocketManager.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/frontend/src/shared/state/dashboardLayoutSlice.ts b/frontend/src/shared/state/dashboardLayoutSlice.ts index 85364346..5c70d0b7 100644 --- a/frontend/src/shared/state/dashboardLayoutSlice.ts +++ b/frontend/src/shared/state/dashboardLayoutSlice.ts @@ -419,6 +419,7 @@ export function placeBesideCard( exclude?: CardPlacementExclusion, gap: number = GRID_GAP * 12, exact: boolean = false, + overlap: boolean = false, ): { x: number; y: number } { const rects = collectOccupiedRects(state, expandedSessionIds, exclude); const targetX = anchor.x + anchor.width + gap; @@ -434,6 +435,8 @@ export function placeBesideCard( ? Math.max(...columnCards.map((c) => c.y + c.height)) + GRID_GAP : anchor.y; + // overlap: dock beside the anchor no matter what else is there (the new card sits on top via its higher zOrder). A chat-spawned browser must land next to its chat even onto an occupied spot; dodging to a free grid cell is the "spawned off to the side" behavior we're removing. Still stacks under this chat's own browser column (targetY) so sibling browsers don't cover each other. + if (overlap) return { x: targetX, y: targetY }; // exact keeps the precise gap (so the card mirrors however its anchor was placed, e.g. a run browser matching the hub->monitor gap); grid-snapping would knock that gap off. Fall back to the snapped search only if the exact spot is taken. if (exact && !rects.some((r) => rectsOverlap({ x: targetX, y: targetY, w: newW, h: newH }, r))) { return { x: targetX, y: targetY }; @@ -1652,6 +1655,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 = placeBesideCard(state, parentCard, bc.width, bc.height, undefined, { type: 'browser' as const, id: bc.browser_id }, undefined, false, true); + 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 9771608f..c4507e17 100644 --- a/frontend/src/shared/ws/WebSocketManager.ts +++ b/frontend/src/shared/ws/WebSocketManager.ts @@ -791,7 +791,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 = placeBesideCard(layoutState, parentCard, browserCard.width, browserCard.height, undefined, exclude, undefined, false, true); } 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) {