diff --git a/frontend/src/app/pages/AgentChat/AgentChat.tsx b/frontend/src/app/pages/AgentChat/AgentChat.tsx index b12c2d49..a600a792 100644 --- a/frontend/src/app/pages/AgentChat/AgentChat.tsx +++ b/frontend/src/app/pages/AgentChat/AgentChat.tsx @@ -2407,7 +2407,7 @@ const AgentChat: React.FC = ({ sessionId: sessionIdProp, onClose data-browser-slot={id} sx={{ flexShrink: 0, - height: 320, + height: 'clamp(240px, 42%, 560px)', mx: 1.5, mb: 1, borderRadius: '10px', diff --git a/frontend/src/app/pages/Dashboard/cards/AgentCard.tsx b/frontend/src/app/pages/Dashboard/cards/AgentCard.tsx index 755961f7..467b92fe 100644 --- a/frontend/src/app/pages/Dashboard/cards/AgentCard.tsx +++ b/frontend/src/app/pages/Dashboard/cards/AgentCard.tsx @@ -710,10 +710,10 @@ const AgentCard: React.FC = ({ if ((session.messages || []).length === 0) dispatch(fetchSession(session.id)); }, [pillMode, session.messages, session.id, dispatch]); - // f7's collapsed state: a session that spawned a browser shows that window under the pill. + // f7's collapsed state: a session's browser (spawned by it or docked into it) shows under the pill. const spawnedBrowserId = useAppSelector((s) => { for (const bc of Object.values(s.dashboardLayout.browserCards)) { - if (bc.spawned_by === session.id) return bc.browser_id; + if (bc.spawned_by === session.id || bc.docked_to === session.id) return bc.browser_id; } return null; }); diff --git a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx index 6b670bea..6155b559 100644 --- a/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx +++ b/frontend/src/app/pages/Dashboard/cards/BrowserCard.tsx @@ -973,6 +973,9 @@ const BrowserCard: React.FC = ({ : c.shadow.md; const dockActive = !!dockRect && !dragging && !localResize && !tiledStyle && !keepAliveHidden && !isMinimized; + // Chat collapsed: its docked browser parks off-screen and lives on as the pill's live shot, + // instead of teleporting back to wherever it sat before docking. + const dockParked = !!dockedTo && !!dockParentCard && !dockParentExpanded && !dragging && !tiledStyle && !isMinimized && !keepAliveHidden; return ( = ({ data-select-id={browserId} data-select-meta={JSON.stringify({ name: activeTitle || 'Browser', url: activeUrl })} // Marks a kept-alive card parked off-screen (it belongs to another dashboard); fit-to-view must skip it or it pans the canvas to chase it and the card bleeds onto the dashboard you're viewing. - data-keepalive-hidden={keepAliveHidden || isMinimized ? '1' : undefined} + data-keepalive-hidden={keepAliveHidden || isMinimized || dockParked ? '1' : undefined} onContextMenu={(e: React.MouseEvent) => openCardContextMenu(e, { items: [ { label: 'New Tab', onClick: () => dispatch(addBrowserTab({ browserId, url: browserHomepage })) }, @@ -1011,13 +1014,13 @@ const BrowserCard: React.FC = ({ sx={{ position: 'absolute', // Kept-alive card from another dashboard: parked far off-screen so its webview surface can't bleed onto the dashboard you're viewing; click-through, webContents stays mounted. - pointerEvents: keepAliveHidden || isMinimized ? 'none' : undefined, + pointerEvents: keepAliveHidden || isMinimized || dockParked ? 'none' : undefined, // contain: webview repaints don't shake neighbor cards. contain: 'layout style', // Own compositor layer so hover/paint invalidations stay contained to this card. See AgentCard for full rationale. willChange: 'transform', - left: keepAliveHidden || isMinimized ? -100000 : (tiledStyle ? tiledStyle.left : dockActive ? dockRect!.x : (dragging ? cardX : displayX)), - top: tiledStyle && !(keepAliveHidden || isMinimized) ? tiledStyle.top : dockActive ? dockRect!.y : (dragging ? cardY : displayY), + left: keepAliveHidden || isMinimized || dockParked ? -100000 : (tiledStyle ? tiledStyle.left : dockActive ? dockRect!.x : (dragging ? cardX : displayX)), + top: tiledStyle && !(keepAliveHidden || isMinimized || dockParked) ? tiledStyle.top : dockActive ? dockRect!.y : (dragging ? cardY : displayY), transform: tiledStyle ? tiledStyle.transform : (dragging ? `translate3d(${dragTx}px, ${dragTy}px, 0)` : undefined), transformOrigin: tiledStyle ? tiledStyle.transformOrigin : undefined, width: tiledStyle ? tiledStyle.width : dockActive ? dockRect!.w : displayW, diff --git a/frontend/src/app/pages/Dashboard/desktop/dockEntries.tsx b/frontend/src/app/pages/Dashboard/desktop/dockEntries.tsx index e0823568..5a923eb1 100644 --- a/frontend/src/app/pages/Dashboard/desktop/dockEntries.tsx +++ b/frontend/src/app/pages/Dashboard/desktop/dockEntries.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import Typography from '@mui/material/Typography'; import LanguageIcon from '@mui/icons-material/Language'; +import GridViewRoundedIcon from '@mui/icons-material/GridViewRounded'; import EditNoteIcon from '@mui/icons-material/EditNote'; import CalendarMonthIcon from '@mui/icons-material/CalendarMonth'; import { MessageCircle } from 'lucide-react'; @@ -96,11 +96,8 @@ export function buildDockEntries({ sessions, cards, viewCards, browserCards, not label: appName, rect: vc, tileBg: 'linear-gradient(135deg, #ef9552, #d96a2b)', - icon: ( - - {appName.charAt(0).toUpperCase()} - - ), + // Never letters in the dock: a real symbol reads as an app, a glyph initial reads as a bug. + icon: , thumbnail: output?.thumbnail, }); } diff --git a/frontend/src/app/pages/Dashboard/geometry/dashboardTethers.ts b/frontend/src/app/pages/Dashboard/geometry/dashboardTethers.ts index ef8f6640..b9a9bc31 100644 --- a/frontend/src/app/pages/Dashboard/geometry/dashboardTethers.ts +++ b/frontend/src/app/pages/Dashboard/geometry/dashboardTethers.ts @@ -261,6 +261,8 @@ export function useTethers({ // An "app:" glow key targets a VIEW card (AppAgent driving an app); everything else is a browser card. const glowTarget = (id: string) => (id.startsWith('app:') ? viewCards[id.slice(4)] : browserCards[id]); for (const [browserId, { sourceId, fading, label }] of Object.entries(glowingBrowserCards)) { + // A docked browser renders INSIDE its chat; an arrow from the chat to it points at nothing. + if (browserCards[browserId]?.docked_to) continue; const t = cardTether( glowTarget(browserId), browserId, diff --git a/frontend/src/app/pages/Settings/sections/general/GeneralInterface.tsx b/frontend/src/app/pages/Settings/sections/general/GeneralInterface.tsx index 47757b29..b0d9cb22 100644 --- a/frontend/src/app/pages/Settings/sections/general/GeneralInterface.tsx +++ b/frontend/src/app/pages/Settings/sections/general/GeneralInterface.tsx @@ -72,7 +72,7 @@ const GeneralInterface: React.FC<{ Dictation - How the mic button and the mic key (F5) work. + {`How the mic button and the dictation shortcut (${/Mac/.test(navigator.platform) ? '\u2318\u21e7D' : 'Ctrl+Shift+D'}) work.`} ; viewCards: Record }, sessionId: string): void { + for (const bc of Object.values(state.browserCards)) { + if (bc.docked_to === sessionId) bc.docked_to = null; + } + for (const vc of Object.values(state.viewCards)) { + if (vc.docked_to === sessionId) vc.docked_to = null; + } +} + const dashboardLayoutSlice = createSlice({ name: 'dashboardLayout', initialState, @@ -852,7 +863,7 @@ const dashboardLayoutSlice = createSlice({ zOrder: state.nextZOrder++, parent_session_id: parentSessionId || null, // An agent-built app defaults to living INSIDE its chat, same as spawned browsers. - docked_to: parentSessionId || null, + docked_to: (parentSessionId && (clearOtherDocks(state, parentSessionId), parentSessionId)) || null, preview_deferred: previewDeferred || undefined, }; state.pendingFocusViewCardId = cardKey; @@ -938,11 +949,15 @@ const dashboardLayoutSlice = createSlice({ setViewDocked(state, action: PayloadAction<{ cardKey: string; dockedTo: string | null }>) { const vc = state.viewCards[action.payload.cardKey]; - if (vc) vc.docked_to = action.payload.dockedTo; + if (!vc) return; + if (action.payload.dockedTo) clearOtherDocks(state, action.payload.dockedTo); + vc.docked_to = action.payload.dockedTo; }, setBrowserDocked(state, action: PayloadAction<{ browserId: string; dockedTo: string | null }>) { const bc = state.browserCards[action.payload.browserId]; - if (bc) bc.docked_to = action.payload.dockedTo; + if (!bc) return; + if (action.payload.dockedTo) clearOtherDocks(state, action.payload.dockedTo); + bc.docked_to = action.payload.dockedTo; }, addBrowserCardFromBackend(state, action: PayloadAction) { const card = action.payload; @@ -1801,6 +1816,9 @@ const dashboardLayoutSlice = createSlice({ 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); + // Default home is inside the chat, same as the non-racing spawn path. + clearOtherDocks(state, session.id); + bc.docked_to = session.id; bc.x = pos.x; bc.y = pos.y; state.glowingBrowserCards[bc.browser_id] = { sourceId: session.id, fading: false, label: 'Use Browser' };