agent-ecosystem/src/features/agent-graph/renderer/ui/TeamGraphOverlay.tsx
2026-05-21 01:10:48 +03:00

235 lines
8.9 KiB
TypeScript

/**
* TeamGraphOverlay — full-screen overlay showing the agent graph.
* Follows the exact ProjectEditorOverlay pattern (lazy-loaded, fixed z-50).
*/
import { useCallback, useState } from 'react';
import { GraphView } from '@claude-teams/agent-graph';
import { TeamSidebarHost } from '@renderer/components/team/sidebar/TeamSidebarHost';
import { useGraphMessagesPanel } from '../hooks/useGraphMessagesPanel';
import { useGraphSidebarVisibility } from '../hooks/useGraphSidebarVisibility';
import { useGraphSurfaceInteractions } from '../hooks/useGraphSurfaceInteractions';
import { useTeamGraphAdapter } from '../hooks/useTeamGraphAdapter';
import { useTeamGraphSurfaceActions } from '../hooks/useTeamGraphSurfaceActions';
import { GraphActivityHud } from './GraphActivityHud';
import { GraphBlockingEdgePopover } from './GraphBlockingEdgePopover';
import { GraphMemberLogPreviewHud } from './GraphMemberLogPreviewHud';
import { GraphNodePopover } from './GraphNodePopover';
import { GraphProvisioningHud } from './GraphProvisioningHud';
import { GraphTransientHandoffHud } from './GraphTransientHandoffHud';
import type {
GraphDomainRef,
GraphEventPort,
TransientHandoffCard,
} from '@claude-teams/agent-graph';
export interface TeamGraphOverlayProps {
teamName: string;
onClose: () => void;
onPinAsTab?: () => void;
sidebarVisible?: boolean;
onToggleSidebar?: () => void;
messagesPanelEnabled?: boolean;
}
export const TeamGraphOverlay = ({
teamName,
onClose,
onPinAsTab,
sidebarVisible,
onToggleSidebar,
messagesPanelEnabled = true,
}: TeamGraphOverlayProps): React.JSX.Element => {
const graphData = useTeamGraphAdapter(teamName);
const {
openTeamPage: openTeamTab,
commitOwnerSlotDrop,
commitOwnerGridOrderDrop,
setLayoutMode,
} = useTeamGraphSurfaceActions(teamName);
const { sidebarVisible: persistedSidebarVisible, toggleSidebarVisible } =
useGraphSidebarVisibility();
const interactions = useGraphSurfaceInteractions(teamName);
const [messagesPanelMountPoint, setMessagesPanelMountPoint] = useState<HTMLDivElement | null>(
null
);
const effectiveSidebarVisible = sidebarVisible ?? persistedSidebarVisible;
const handleToggleSidebar = onToggleSidebar ?? toggleSidebarVisible;
const graphMessagesPanel = useGraphMessagesPanel({
teamName,
enabled: messagesPanelEnabled,
mountPoint: messagesPanelMountPoint,
onOpenMemberProfile: interactions.openMemberProfile,
onOpenTaskDetail: interactions.openTaskDetail,
});
const openTeamPage = useCallback(() => {
openTeamTab();
onClose();
}, [onClose, openTeamTab]);
const openCreateTask = useCallback(() => {
interactions.openCreateTask('');
}, [interactions]);
const events: GraphEventPort = {
onNodeDoubleClick: useCallback(
(ref: GraphDomainRef) => {
if (ref.kind === 'task') interactions.openTaskDetail(ref.taskId);
else if (ref.kind === 'member') interactions.openMemberProfile(ref.memberName);
},
[interactions]
),
onSendMessage: interactions.openSendMessage,
onOpenTaskDetail: interactions.openTaskDetail,
onOpenMemberProfile: useCallback(
(memberName: string) => interactions.openMemberProfile(memberName),
[interactions]
),
};
return (
<div className="fixed inset-0 z-50 flex overflow-hidden" style={{ background: '#050510' }}>
{effectiveSidebarVisible ? (
<TeamSidebarHost teamName={teamName} surface="graph-overlay" isActive isFocused />
) : null}
<GraphView
data={graphData}
events={events}
isSurfaceActive
onRequestClose={onClose}
onRequestPinAsTab={onPinAsTab}
onOpenTeamPage={openTeamPage}
onCreateTask={openCreateTask}
onToggleSidebar={handleToggleSidebar}
isSidebarVisible={effectiveSidebarVisible}
renderTopToolbarContent={() => <GraphProvisioningHud teamName={teamName} />}
onLayoutModeChange={setLayoutMode}
onOwnerSlotDrop={commitOwnerSlotDrop}
onOwnerGridOrderDrop={commitOwnerGridOrderDrop}
className="team-graph-view min-w-0 flex-1"
renderHud={(hudProps) => {
const extraHudProps = hudProps as typeof hudProps & {
getViewportSize?: () => { width: number; height: number };
getActivityWorldRect?: (ownerNodeId: string) => {
left: number;
top: number;
right: number;
bottom: number;
width: number;
height: number;
} | null;
getLogWorldRect?: (ownerNodeId: string) => {
left: number;
top: number;
right: number;
bottom: number;
width: number;
height: number;
} | null;
getCameraZoom?: () => number;
getTransientHandoffSnapshot?: (options?: {
focusNodeIds?: ReadonlySet<string> | null;
focusEdgeIds?: ReadonlySet<string> | null;
}) => {
cards: TransientHandoffCard[];
time: number;
};
worldToScreen?: (x: number, y: number) => { x: number; y: number };
getNodeWorldPosition?: (nodeId: string) => { x: number; y: number } | null;
focusEdgeIds?: ReadonlySet<string> | null;
};
const { getViewportSize, focusNodeIds, filters } = extraHudProps;
return (
<>
<GraphTransientHandoffHud
teamName={teamName}
getTransientHandoffSnapshot={extraHudProps.getTransientHandoffSnapshot}
getCameraZoom={extraHudProps.getCameraZoom}
worldToScreen={extraHudProps.worldToScreen}
getNodeWorldPosition={extraHudProps.getNodeWorldPosition}
focusNodeIds={focusNodeIds}
focusEdgeIds={extraHudProps.focusEdgeIds ?? null}
/>
<GraphActivityHud
teamName={teamName}
nodes={graphData.nodes}
getActivityWorldRect={extraHudProps.getActivityWorldRect}
getCameraZoom={extraHudProps.getCameraZoom}
worldToScreen={extraHudProps.worldToScreen}
getNodeWorldPosition={extraHudProps.getNodeWorldPosition}
getViewportSize={getViewportSize}
focusNodeIds={focusNodeIds}
enabled={filters?.showActivity ?? true}
showConnectors={filters?.showEdges ?? false}
onOpenTaskDetail={interactions.openTaskDetail}
onOpenMemberProfile={interactions.openMemberProfile}
/>
<GraphMemberLogPreviewHud
teamName={teamName}
nodes={graphData.nodes}
getLogWorldRect={extraHudProps.getLogWorldRect}
getCameraZoom={extraHudProps.getCameraZoom}
worldToScreen={extraHudProps.worldToScreen}
getViewportSize={getViewportSize}
focusNodeIds={focusNodeIds}
enabled={filters?.showLogs ?? true}
onOpenMemberProfile={interactions.openMemberProfile}
/>
</>
);
}}
renderEdgeOverlay={({ edge, sourceNode, targetNode, onClose: closeEdge, onSelectNode }) => (
<GraphBlockingEdgePopover
teamName={teamName}
edge={edge}
sourceNode={sourceNode}
targetNode={targetNode}
onClose={closeEdge}
onSelectNode={onSelectNode}
onOpenTaskDetail={interactions.openTaskDetail}
/>
)}
renderOverlay={({ node, onClose: closePopover }) => (
<GraphNodePopover
node={node}
teamName={teamName}
onClose={closePopover}
onSendMessage={(name) => {
interactions.openSendMessage(name);
closePopover();
}}
onCreateTask={interactions.openCreateTask}
onOpenTaskDetail={(id) => {
interactions.openTaskDetail(id);
closePopover();
}}
onOpenMemberProfile={(name, options) => {
interactions.openMemberProfile(name, options);
closePopover();
}}
onStartTask={interactions.onStartTask}
onCompleteTask={interactions.onCompleteTask}
onApproveTask={interactions.onApproveTask}
onRequestReview={interactions.onRequestReview}
onRequestChanges={interactions.onRequestChanges}
onCancelTask={interactions.onCancelTask}
onMoveBackToDone={interactions.onMoveBackToDone}
onViewChanges={interactions.openTaskChanges}
onDeleteTask={interactions.onDeleteTask}
/>
)}
/>
{messagesPanelEnabled ? (
<div
ref={setMessagesPanelMountPoint}
className="pointer-events-none absolute inset-0 z-30"
/>
) : null}
{graphMessagesPanel}
{interactions.dialogs}
</div>
);
};