[eric] chat: the docked mini rides the drag channel frame-locked, one owner per dock, docked glow buys no per-frame tether work

This commit is contained in:
ciregenz
2026-08-04 20:12:15 -07:00
parent ee2847574c
commit 9cfdf390b4
3 changed files with 50 additions and 12 deletions
+12 -7
View File
@@ -258,13 +258,18 @@ const AgentChat: React.FC<AgentChatProps> = ({ sessionId: sessionIdProp, onClose
const hasDockedBrowser = useAppSelector((st) =>
Object.values(st.dashboardLayout.browserCards).some((bc) => bc.docked_to === (sessionIdProp || routeId)) ||
Object.values(st.dashboardLayout.viewCards).some((vc) => vc.docked_to === (sessionIdProp || routeId)));
// The docked surface's aspect ratio, so the inline slot hugs the browser's shape instead of reserving a fixed letterbox band (primitive selectors so no fresh-object rerenders).
const dockedSurfaceW = useAppSelector((st) =>
Object.values(st.dashboardLayout.browserCards).find((bc) => bc.docked_to === (sessionIdProp || routeId))?.width ?? 0);
const dockedSurfaceH = useAppSelector((st) =>
Object.values(st.dashboardLayout.browserCards).find((bc) => bc.docked_to === (sessionIdProp || routeId))?.height ?? 0);
const dockedSurfaceId = useAppSelector((st) =>
Object.values(st.dashboardLayout.browserCards).find((bc) => bc.docked_to === (sessionIdProp || routeId))?.browser_id ?? null);
// The docked surface's aspect ratio, so the inline slot hugs the browser's shape instead of reserving a fixed letterbox band (primitive selectors so no fresh-object rerenders). Highest z wins, mirroring BrowserCard's dock-owner election, so a dead rival's stale dock never feeds dims or shots.
const pickTopDocked = (st: { dashboardLayout: { browserCards: Record<string, { browser_id: string; docked_to?: string | null; width: number; height: number; zOrder: number }> } }) => {
let best: { browser_id: string; width: number; height: number; zOrder: number } | null = null;
for (const b of Object.values(st.dashboardLayout.browserCards)) {
if (b.docked_to !== (sessionIdProp || routeId)) continue;
if (!best || (b.zOrder || 0) > (best.zOrder || 0)) best = b;
}
return best;
};
const dockedSurfaceW = useAppSelector((st) => pickTopDocked(st)?.width ?? 0);
const dockedSurfaceH = useAppSelector((st) => pickTopDocked(st)?.height ?? 0);
const dockedSurfaceId = useAppSelector((st) => pickTopDocked(st)?.browser_id ?? null);
// Shared by the anchor slot and the fallback slot so both read as the same framed block.
const browserSlotSx = {
position: 'relative',
@@ -9,8 +9,10 @@ import TetherLayer from './TetherLayer';
// whole dashboard tree (the ENG-88 input delay).
const TetherLayerHost: React.FC<{ inputs: TetherInputs; c: ClaudeTokens }> = ({ inputs, c }) => {
const [liveDrag, setLiveDrag] = useState<LiveDragInfo | null>(null);
// Tethers only exist while something glows; on an idle canvas a drag frame should cost zero React work, so don't even subscribe.
const hasTethers = Object.keys(inputs.glowingAgentCards).length > 0 || Object.keys(inputs.glowingBrowserCards).length > 0;
// Tethers only exist while something glows AND that something is on the canvas; a DOCKED browser's glow draws no tether (geometry skips docked cards), so dragging its chat must not buy per-frame React either.
const hasTethers =
Object.keys(inputs.glowingAgentCards).length > 0 ||
Object.keys(inputs.glowingBrowserCards).some((bid) => !inputs.browserCards[bid]?.docked_to);
useEffect(() => {
if (!hasTethers) { setLiveDrag(null); return undefined; }
return subscribeLiveDrag(setLiveDrag);
@@ -1,5 +1,6 @@
import React, { useState, useRef, useCallback, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { subscribeLiveDrag } from '../hooks/interaction/liveDragChannel';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
@@ -317,6 +318,23 @@ const BrowserCard: React.FC<Props> = ({
}, [dockedTo, dockParentExpanded, dockParentTiled, dockParentCard?.x, dockParentCard?.y, dockParentCard?.width, dockParentCard?.height, getCanvasState, dockParentCard]);
const dockParentZ = dockParentCard?.zOrder ?? 0;
// The chat drags on a per-frame compositor transform, but dock geometry only re-measures at settle; ride the same drag channel imperatively or the mini visibly trails its own chat.
const hasDockRect = !!dockRect;
useEffect(() => {
if (!dockedTo || !hasDockRect) return undefined;
const off = subscribeLiveDrag((info) => {
const el = rootElRef.current;
if (!el) return;
if (info && info.cardId === dockedTo) el.style.translate = `${info.dx}px ${info.dy}px`;
else el.style.translate = '';
});
return () => {
off();
const el = rootElRef.current;
if (el) el.style.translate = '';
};
}, [dockedTo, hasDockRect]);
const suspendedSnap = useAppSelector((state) => state.dashboardLayout.suspendedBrowserCards[browserId]);
const endingState = useAppSelector((state) => state.dashboardLayout.endingBrowserCards[browserId]);
@@ -1048,9 +1066,22 @@ const BrowserCard: React.FC<Props> = ({
? `0 0 0 1px #3b82f6, ${c.shadow.md}`
: c.shadow.md;
const dockActive = !!dockRect && !dragging && !localResize && !isTiled && !keepAliveHidden && !isMinimized;
// Docked in intent but no slot rect yet (slot not mounted, or windowed out before first measure): hide rather than flash the card at its stale canvas home.
const dockPending = !!dockedTo && !!dockParentCard && dockParentExpanded && !dockRect && !dragging && !isTiled && !isMinimized && !keepAliveHidden;
// Exactly one card owns a chat's dock (highest z wins): a dead browser whose dock the layout sync re-asserts must never stack under its replacement in the same slot.
const dockOwnerId = useAppSelector((state) => {
if (!dockedTo) return null;
let bestId: string | null = null;
let bestZ = -Infinity;
for (const b of Object.values(state.dashboardLayout.browserCards)) {
if (b.docked_to !== dockedTo) continue;
const z = b.zOrder || 0;
if (z > bestZ) { bestZ = z; bestId = b.browser_id; }
}
return bestId;
});
const isDockOwner = dockOwnerId === browserId;
const dockActive = !!dockRect && isDockOwner && !dragging && !localResize && !isTiled && !keepAliveHidden && !isMinimized;
// Docked in intent but no slot rect yet (slot not mounted, or windowed out before first measure), OR docked but out-elected by a newer dock owner: hide rather than flash the card at its stale canvas home.
const dockPending = !!dockedTo && !!dockParentCard && (!isDockOwner || (dockParentExpanded && !dockRect)) && !dragging && !isTiled && !isMinimized && !keepAliveHidden;
// An agent can only SEE a page the compositor is drawing, and Chromium draws nothing at all for a
// guest parked at left:-100000. Measured in one window: a card on screen captured in 58ms while
// the same card parked timed out on guest capturePage, on host capturePage AND on CDP