mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-02 14:28:59 +02:00
[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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user