diff --git a/frontend/src/app/pages/Dashboard/cards/DashboardViewCard.tsx b/frontend/src/app/pages/Dashboard/cards/DashboardViewCard.tsx index 48f10718..6270a2af 100644 --- a/frontend/src/app/pages/Dashboard/cards/DashboardViewCard.tsx +++ b/frontend/src/app/pages/Dashboard/cards/DashboardViewCard.tsx @@ -132,6 +132,9 @@ const DashboardViewCard: React.FC = ({ const scrollOverlayRef = useOverlayScrollPassthrough(isSelected); const previewRef = useRef(null); const activeViewCardId = useAppSelector((s) => s.dashboardLayout.activeViewCardId); + // Agent-driving glow, same treatment as browser cards: an AppAgent session carries browser_id "app:", which keys glowingBrowserCards. + const appGlow = useAppSelector((s) => s.dashboardLayout.glowingBrowserCards[`app:${cardKeyProp ?? output.id}`]); + const showAgentGlow = !!appGlow && !appGlow.fading; const interactive = activeViewCardId === cardKey; // Deselecting the card exits interact mode (click anywhere else on canvas). @@ -445,22 +448,26 @@ const DashboardViewCard: React.FC = ({ borderRadius: `${c.radius.lg}px`, border: isHighlighted ? `2px solid ${c.accent.primary}` - : interactive + : showAgentGlow ? `2px solid ${c.accent.primary}` - : isSelected ? '2px solid #3b82f6' : `1px solid ${c.border.medium}`, + : interactive + ? `2px solid ${c.accent.primary}` + : isSelected ? '2px solid #3b82f6' : `1px solid ${c.border.medium}`, bgcolor: c.bg.surface, boxShadow: isHighlighted ? `0 0 0 3px ${c.accent.primary}50, 0 0 20px ${c.accent.primary}35, 0 0 40px ${c.accent.primary}15` - : isDragging || isResizing - ? c.shadow.lg - : isSelected - ? `0 0 0 1px #3b82f6, ${c.shadow.md}` - : c.shadow.md, + : showAgentGlow + ? `0 0 0 2px ${c.accent.primary}40, 0 0 18px ${c.accent.primary}30, 0 0 40px ${c.accent.primary}15, inset 0 0 30px ${c.accent.primary}25` + : isDragging || isResizing + ? c.shadow.lg + : isSelected + ? `0 0 0 1px #3b82f6, ${c.shadow.md}` + : c.shadow.md, overflow: 'hidden', display: 'flex', flexDirection: 'column', zIndex: (isDragging || isResizing) ? 999999 : cardZOrder, - transition: noTransition ? 'none' : 'box-shadow 0.2s', + transition: noTransition ? 'none' : 'box-shadow 0.4s ease, border 0.3s ease', '&:hover .resize-handle': { opacity: 1 }, ...(isHighlighted && { animation: 'card-highlight-pulse 2s ease-out forwards', diff --git a/frontend/src/app/pages/Dashboard/geometry/dashboardTethers.ts b/frontend/src/app/pages/Dashboard/geometry/dashboardTethers.ts index d168c5cf..ef8f6640 100644 --- a/frontend/src/app/pages/Dashboard/geometry/dashboardTethers.ts +++ b/frontend/src/app/pages/Dashboard/geometry/dashboardTethers.ts @@ -258,9 +258,11 @@ export function useTethers({ } const glowTethers = new Map>(); + // 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)) { const t = cardTether( - browserCards[browserId], + glowTarget(browserId), browserId, sourceId, `browser-${browserId}`, @@ -278,7 +280,7 @@ export function useTethers({ // A browser docked below the hub keeps a "Browser" pointer so the link reads at a glance; the right-docked agent/run cases stay label-free (their glow already said it on spawn). const parent = sessionById.get(s.parent_session_id); const t = cardTether( - browserCards[s.browser_id], + glowTarget(s.browser_id), s.browser_id, s.parent_session_id, `browser-${s.browser_id}`, diff --git a/frontend/src/shared/ws/WebSocketManager.ts b/frontend/src/shared/ws/WebSocketManager.ts index 78f812b4..1211a751 100644 --- a/frontend/src/shared/ws/WebSocketManager.ts +++ b/frontend/src/shared/ws/WebSocketManager.ts @@ -384,6 +384,17 @@ class WebSocketManager { store.dispatch(trackAgentNotification(session_id)); } + // An AppAgent driving an app card announces itself only via this status event (no card_added like browsers), so light the app card here. Keyed by the parent chat like browser glows, so the same terminal fade below clears it. + const p_sess = data.session; + if (p_sess && p_sess.mode === 'browser-agent' && typeof p_sess.browser_id === 'string' && p_sess.browser_id.startsWith('app:') + && (p_sess.status === 'running' || p_sess.status === 'waiting_approval')) { + store.dispatch(setGlowingBrowserCards({ + browserIds: [p_sess.browser_id], + sessionId: p_sess.parent_session_id || p_sess.id, + label: 'Use App', + })); + } + // Fade this session's browser glows on the terminal transition HERE, not only in AgentChat's effect: a collapsed chat is unmounted at finish, and a never-faded glow pins the browser's renderer (exempt from suspend + the webview cap) forever. const newStatus = data.status ?? data.session?.status; const wasWorking = prevStatus === 'running' || prevStatus === 'waiting_approval';