diff --git a/packages/agent-graph/src/ui/GraphControls.tsx b/packages/agent-graph/src/ui/GraphControls.tsx index fb5806eb..884817d1 100644 --- a/packages/agent-graph/src/ui/GraphControls.tsx +++ b/packages/agent-graph/src/ui/GraphControls.tsx @@ -6,6 +6,7 @@ import { useCallback } from 'react'; import { Columns3, + Expand, Eye, EyeOff, Maximize2, @@ -33,6 +34,7 @@ export interface GraphControlsProps { onZoomToFit: () => void; onRequestClose?: () => void; onRequestPinAsTab?: () => void; + onRequestFullscreen?: () => void; teamName: string; teamColor?: string; isAlive?: boolean; @@ -46,6 +48,7 @@ export function GraphControls({ onZoomToFit, onRequestClose, onRequestPinAsTab, + onRequestFullscreen, teamName, teamColor, isAlive, @@ -133,6 +136,16 @@ export function GraphControls({ /> )} + {onRequestFullscreen && ( + <> + + } + label="Fullscreen" + /> + + )} {onRequestClose && ( } /> )} diff --git a/packages/agent-graph/src/ui/GraphView.tsx b/packages/agent-graph/src/ui/GraphView.tsx index bfac037f..4dec015f 100644 --- a/packages/agent-graph/src/ui/GraphView.tsx +++ b/packages/agent-graph/src/ui/GraphView.tsx @@ -31,6 +31,7 @@ export interface GraphViewProps { className?: string; onRequestClose?: () => void; onRequestPinAsTab?: () => void; + onRequestFullscreen?: () => void; } export function GraphView({ @@ -40,6 +41,7 @@ export function GraphView({ className, onRequestClose, onRequestPinAsTab, + onRequestFullscreen, }: GraphViewProps): React.JSX.Element { // ─── React state (user-facing only) ───────────────────────────────────── const [selectedNodeId, setSelectedNodeId] = useState(null); @@ -327,6 +329,7 @@ export function GraphView({ }} onRequestClose={onRequestClose} onRequestPinAsTab={onRequestPinAsTab} + onRequestFullscreen={onRequestFullscreen} teamName={data.teamName} teamColor={data.teamColor} isAlive={data.isAlive} diff --git a/src/renderer/components/team/TeamDetailView.tsx b/src/renderer/components/team/TeamDetailView.tsx index 4a01b5c0..878c9565 100644 --- a/src/renderer/components/team/TeamDetailView.tsx +++ b/src/renderer/components/team/TeamDetailView.tsx @@ -214,18 +214,46 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele } }, [editorOpen, graphOpen]); - // Listen for Cmd+Shift+G keyboard shortcut + // Listen for Cmd+Shift+G keyboard shortcut — opens graph tab useEffect(() => { const handler = (e: Event) => { const detail = (e as CustomEvent).detail; if (detail?.teamName === teamName) { - setGraphOpen((prev) => !prev); + useStore.getState().openTab({ + type: 'graph', + label: `${teamName} Graph`, + teamName, + }); } }; window.addEventListener('toggle-team-graph', handler); return () => window.removeEventListener('toggle-team-graph', handler); }, [teamName]); + // Listen for graph tab actions (open task, send message) + useEffect(() => { + const onOpenTask = (e: Event) => { + const { teamName: tn, taskId } = (e as CustomEvent).detail ?? {}; + if (tn !== teamName || !data) return; + const task = data.tasks.find((t: { id: string }) => t.id === taskId); + if (task) setSelectedTask(task); + }; + const onSendMsg = (e: Event) => { + const { teamName: tn, memberName } = (e as CustomEvent).detail ?? {}; + if (tn !== teamName) return; + setSendDialogRecipient(memberName); + setSendDialogDefaultText(undefined); + setSendDialogDefaultChip(undefined); + setSendDialogOpen(true); + }; + window.addEventListener('graph:open-task', onOpenTask); + window.addEventListener('graph:send-message', onSendMsg); + return () => { + window.removeEventListener('graph:open-task', onOpenTask); + window.removeEventListener('graph:send-message', onSendMsg); + }; + }); + const [sendDialogOpen, setSendDialogOpen] = useState(false); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [stoppingTeam, setStoppingTeam] = useState(false); @@ -1432,7 +1460,11 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele className="h-6 gap-1 px-2 text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text)]" onClick={(e) => { e.stopPropagation(); - setGraphOpen(true); + useStore.getState().openTab({ + type: 'graph', + label: `${data.config.name} Graph`, + teamName, + }); }} > diff --git a/src/renderer/features/agent-graph/ui/TeamGraphTab.tsx b/src/renderer/features/agent-graph/ui/TeamGraphTab.tsx index 3238ea49..041bac55 100644 --- a/src/renderer/features/agent-graph/ui/TeamGraphTab.tsx +++ b/src/renderer/features/agent-graph/ui/TeamGraphTab.tsx @@ -1,8 +1,9 @@ /** * TeamGraphTab — wraps GraphView for use as a dedicated tab. + * Provides Fullscreen button that opens the overlay. */ -import { useCallback } from 'react'; +import { useCallback, useState, lazy, Suspense } from 'react'; import { GraphView } from '@claude-teams/agent-graph'; @@ -10,22 +11,73 @@ import { useTeamGraphAdapter } from '../adapters/useTeamGraphAdapter'; import type { GraphDomainRef, GraphEventPort } from '@claude-teams/agent-graph'; +const TeamGraphOverlay = lazy(() => + import('./TeamGraphOverlay').then((m) => ({ default: m.TeamGraphOverlay })) +); + export interface TeamGraphTabProps { teamName: string; } export const TeamGraphTab = ({ teamName }: TeamGraphTabProps): React.JSX.Element => { const graphData = useTeamGraphAdapter(teamName); + const [fullscreen, setFullscreen] = useState(false); const events: GraphEventPort = { - onNodeDoubleClick: useCallback((ref: GraphDomainRef) => { - console.log('Double-click in tab:', ref); - }, []), + onNodeDoubleClick: useCallback( + (ref: GraphDomainRef) => { + // Dispatch to TeamDetailView's dialog system via CustomEvent + if (ref.kind === 'task') { + window.dispatchEvent( + new CustomEvent('graph:open-task', { detail: { teamName, taskId: ref.taskId } }) + ); + } else if (ref.kind === 'member') { + window.dispatchEvent( + new CustomEvent('graph:send-message', { + detail: { teamName, memberName: ref.memberName }, + }) + ); + } + }, + [teamName] + ), + onSendMessage: useCallback( + (memberName: string) => { + window.dispatchEvent( + new CustomEvent('graph:send-message', { detail: { teamName, memberName } }) + ); + }, + [teamName] + ), + onOpenTaskDetail: useCallback( + (taskId: string) => { + window.dispatchEvent(new CustomEvent('graph:open-task', { detail: { teamName, taskId } })); + }, + [teamName] + ), + onOpenMemberProfile: useCallback( + (memberName: string) => { + window.dispatchEvent( + new CustomEvent('graph:send-message', { detail: { teamName, memberName } }) + ); + }, + [teamName] + ), }; return (
- + setFullscreen(true)} + /> + {fullscreen && ( + + setFullscreen(false)} /> + + )}
); };