[eric] dashboard: app cards glow + tether while an AppAgent drives them, same treatment as browser cards (glow keys app:<output_id>, set on the child's status event, fades on parent terminal)

This commit is contained in:
ciregenz
2026-07-16 20:38:30 -07:00
parent ce3e67d613
commit 1ba5f173dc
3 changed files with 30 additions and 10 deletions
@@ -132,6 +132,9 @@ const DashboardViewCard: React.FC<Props> = ({
const scrollOverlayRef = useOverlayScrollPassthrough(isSelected);
const previewRef = useRef<ViewPreviewHandle>(null);
const activeViewCardId = useAppSelector((s) => s.dashboardLayout.activeViewCardId);
// Agent-driving glow, same treatment as browser cards: an AppAgent session carries browser_id "app:<output_id>", 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<Props> = ({
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',
@@ -258,9 +258,11 @@ export function useTethers({
}
const glowTethers = new Map<string, ReturnType<typeof cardTether>>();
// An "app:<output_id>" 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}`,
@@ -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';