agent-ecosystem/src/renderer/features/agent-graph/ui/TeamGraphTab.tsx
2026-04-13 17:00:49 +03:00

194 lines
6.6 KiB
TypeScript

/**
* TeamGraphTab — wraps GraphView for use as a dedicated tab.
* Provides Fullscreen button that opens the overlay.
*/
import { lazy, Suspense, useCallback, useMemo, useState } from 'react';
import { GraphView } from '@claude-teams/agent-graph';
import { TeamSidebarHost } from '@renderer/components/team/sidebar/TeamSidebarHost';
import { useTeamGraphAdapter } from '../adapters/useTeamGraphAdapter';
import { GraphActivityHud } from './GraphActivityHud';
import { GraphBlockingEdgePopover } from './GraphBlockingEdgePopover';
import { GraphNodePopover } from './GraphNodePopover';
import { GraphProvisioningHud } from './GraphProvisioningHud';
import type { GraphDomainRef, GraphEventPort, GraphNode } from '@claude-teams/agent-graph';
const TeamGraphOverlay = lazy(() =>
import('./TeamGraphOverlay').then((m) => ({ default: m.TeamGraphOverlay }))
);
export interface TeamGraphTabProps {
teamName: string;
isActive?: boolean;
isPaneFocused?: boolean;
}
export const TeamGraphTab = ({
teamName,
isActive = true,
isPaneFocused = false,
}: TeamGraphTabProps): React.JSX.Element => {
const graphData = useTeamGraphAdapter(teamName);
const leadNodeId = useMemo(
() => graphData.nodes.find((node) => node.kind === 'lead')?.id ?? null,
[graphData.nodes]
);
const [fullscreen, setFullscreen] = useState(false);
// Typed event dispatchers (DRY — used in both events + renderOverlay)
const dispatchOpenTask = useCallback(
(taskId: string) =>
window.dispatchEvent(new CustomEvent('graph:open-task', { detail: { teamName, taskId } })),
[teamName]
);
const dispatchSendMessage = useCallback(
(memberName: string) =>
window.dispatchEvent(
new CustomEvent('graph:send-message', { detail: { teamName, memberName } })
),
[teamName]
);
const dispatchOpenProfile = useCallback(
(memberName: string) =>
window.dispatchEvent(
new CustomEvent('graph:open-profile', { detail: { teamName, memberName } })
),
[teamName]
);
const dispatchCreateTask = useCallback(
(owner: string) =>
window.dispatchEvent(new CustomEvent('graph:create-task', { detail: { teamName, owner } })),
[teamName]
);
// Task action dispatchers
const dispatchTaskAction = useCallback(
(action: string) => (taskId: string) =>
window.dispatchEvent(new CustomEvent(`graph:${action}`, { detail: { teamName, taskId } })),
[teamName]
);
const dispatchStartTask = useMemo(() => dispatchTaskAction('start-task'), [dispatchTaskAction]);
const dispatchCompleteTask = useMemo(
() => dispatchTaskAction('complete-task'),
[dispatchTaskAction]
);
const dispatchApproveTask = useMemo(
() => dispatchTaskAction('approve-task'),
[dispatchTaskAction]
);
const dispatchRequestReview = useMemo(
() => dispatchTaskAction('request-review'),
[dispatchTaskAction]
);
const dispatchRequestChanges = useMemo(
() => dispatchTaskAction('request-changes'),
[dispatchTaskAction]
);
const dispatchCancelTask = useMemo(() => dispatchTaskAction('cancel-task'), [dispatchTaskAction]);
const dispatchMoveBackToDone = useMemo(
() => dispatchTaskAction('move-back-to-done'),
[dispatchTaskAction]
);
const dispatchDeleteTask = useMemo(() => dispatchTaskAction('delete-task'), [dispatchTaskAction]);
const events: GraphEventPort = {
onNodeDoubleClick: useCallback(
(ref: GraphDomainRef) => {
if (ref.kind === 'task') dispatchOpenTask(ref.taskId);
else if (ref.kind === 'member') dispatchOpenProfile(ref.memberName);
},
[dispatchOpenTask, dispatchOpenProfile]
),
onSendMessage: dispatchSendMessage,
onOpenTaskDetail: dispatchOpenTask,
onOpenMemberProfile: dispatchOpenProfile,
};
return (
<div className="flex size-full overflow-hidden" style={{ background: '#050510' }}>
<TeamSidebarHost
teamName={teamName}
surface="graph-tab"
isActive={isActive}
isFocused={isPaneFocused}
/>
<div className="min-w-0 flex-1">
<GraphView
data={graphData}
events={events}
className="team-graph-view size-full"
suspendAnimation={!isActive}
onRequestFullscreen={() => setFullscreen(true)}
renderHud={({
getLaunchAnchorScreenPlacement,
getActivityAnchorScreenPlacement,
focusNodeIds,
}) => (
<>
<GraphActivityHud
teamName={teamName}
nodes={graphData.nodes}
getActivityAnchorScreenPlacement={getActivityAnchorScreenPlacement}
focusNodeIds={focusNodeIds}
enabled={isActive}
onOpenTaskDetail={dispatchOpenTask}
onOpenMemberProfile={dispatchOpenProfile}
/>
<GraphProvisioningHud
teamName={teamName}
leadNodeId={leadNodeId}
getLaunchAnchorScreenPlacement={getLaunchAnchorScreenPlacement}
enabled={isActive}
/>
</>
)}
renderEdgeOverlay={({ edge, sourceNode, targetNode, onClose, onSelectNode }) => (
<GraphBlockingEdgePopover
teamName={teamName}
edge={edge}
sourceNode={sourceNode}
targetNode={targetNode}
onClose={onClose}
onSelectNode={onSelectNode}
onOpenTaskDetail={dispatchOpenTask}
/>
)}
renderOverlay={({ node, onClose }) => (
<GraphNodePopover
node={node}
teamName={teamName}
onClose={onClose}
onSendMessage={dispatchSendMessage}
onOpenTaskDetail={dispatchOpenTask}
onOpenMemberProfile={dispatchOpenProfile}
onCreateTask={dispatchCreateTask}
onStartTask={dispatchStartTask}
onCompleteTask={dispatchCompleteTask}
onApproveTask={dispatchApproveTask}
onRequestReview={dispatchRequestReview}
onRequestChanges={dispatchRequestChanges}
onCancelTask={dispatchCancelTask}
onMoveBackToDone={dispatchMoveBackToDone}
onDeleteTask={dispatchDeleteTask}
/>
)}
/>
</div>
{fullscreen && (
<Suspense fallback={null}>
<TeamGraphOverlay
teamName={teamName}
onClose={() => setFullscreen(false)}
onSendMessage={dispatchSendMessage}
onOpenTaskDetail={dispatchOpenTask}
onOpenMemberProfile={dispatchOpenProfile}
/>
</Suspense>
)}
</div>
);
};