feat(graph): default to tab mode + fullscreen button in tab
- Graph button opens dedicated tab (was: overlay) - Cmd+Shift+G opens tab (was: toggle overlay) - Tab mode has Fullscreen button → opens overlay on top - Tab actions (Message, Open) dispatch CustomEvents to TeamDetailView which opens corresponding dialogs (SendMessage, TaskDetail) - Overlay still available via Fullscreen button or TeamGraphOverlay
This commit is contained in:
parent
8a9121fc3e
commit
cb17e2158f
4 changed files with 108 additions and 8 deletions
|
|
@ -6,6 +6,7 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import {
|
import {
|
||||||
Columns3,
|
Columns3,
|
||||||
|
Expand,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
Maximize2,
|
Maximize2,
|
||||||
|
|
@ -33,6 +34,7 @@ export interface GraphControlsProps {
|
||||||
onZoomToFit: () => void;
|
onZoomToFit: () => void;
|
||||||
onRequestClose?: () => void;
|
onRequestClose?: () => void;
|
||||||
onRequestPinAsTab?: () => void;
|
onRequestPinAsTab?: () => void;
|
||||||
|
onRequestFullscreen?: () => void;
|
||||||
teamName: string;
|
teamName: string;
|
||||||
teamColor?: string;
|
teamColor?: string;
|
||||||
isAlive?: boolean;
|
isAlive?: boolean;
|
||||||
|
|
@ -46,6 +48,7 @@ export function GraphControls({
|
||||||
onZoomToFit,
|
onZoomToFit,
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
onRequestPinAsTab,
|
onRequestPinAsTab,
|
||||||
|
onRequestFullscreen,
|
||||||
teamName,
|
teamName,
|
||||||
teamColor,
|
teamColor,
|
||||||
isAlive,
|
isAlive,
|
||||||
|
|
@ -133,6 +136,16 @@ export function GraphControls({
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{onRequestFullscreen && (
|
||||||
|
<>
|
||||||
|
<Separator />
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={onRequestFullscreen}
|
||||||
|
icon={<Expand size={13} />}
|
||||||
|
label="Fullscreen"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{onRequestClose && (
|
{onRequestClose && (
|
||||||
<ToolbarButton onClick={onRequestClose} icon={<X size={13} />} />
|
<ToolbarButton onClick={onRequestClose} icon={<X size={13} />} />
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ export interface GraphViewProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
onRequestClose?: () => void;
|
onRequestClose?: () => void;
|
||||||
onRequestPinAsTab?: () => void;
|
onRequestPinAsTab?: () => void;
|
||||||
|
onRequestFullscreen?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GraphView({
|
export function GraphView({
|
||||||
|
|
@ -40,6 +41,7 @@ export function GraphView({
|
||||||
className,
|
className,
|
||||||
onRequestClose,
|
onRequestClose,
|
||||||
onRequestPinAsTab,
|
onRequestPinAsTab,
|
||||||
|
onRequestFullscreen,
|
||||||
}: GraphViewProps): React.JSX.Element {
|
}: GraphViewProps): React.JSX.Element {
|
||||||
// ─── React state (user-facing only) ─────────────────────────────────────
|
// ─── React state (user-facing only) ─────────────────────────────────────
|
||||||
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
||||||
|
|
@ -327,6 +329,7 @@ export function GraphView({
|
||||||
}}
|
}}
|
||||||
onRequestClose={onRequestClose}
|
onRequestClose={onRequestClose}
|
||||||
onRequestPinAsTab={onRequestPinAsTab}
|
onRequestPinAsTab={onRequestPinAsTab}
|
||||||
|
onRequestFullscreen={onRequestFullscreen}
|
||||||
teamName={data.teamName}
|
teamName={data.teamName}
|
||||||
teamColor={data.teamColor}
|
teamColor={data.teamColor}
|
||||||
isAlive={data.isAlive}
|
isAlive={data.isAlive}
|
||||||
|
|
|
||||||
|
|
@ -214,18 +214,46 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
||||||
}
|
}
|
||||||
}, [editorOpen, graphOpen]);
|
}, [editorOpen, graphOpen]);
|
||||||
|
|
||||||
// Listen for Cmd+Shift+G keyboard shortcut
|
// Listen for Cmd+Shift+G keyboard shortcut — opens graph tab
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handler = (e: Event) => {
|
const handler = (e: Event) => {
|
||||||
const detail = (e as CustomEvent).detail;
|
const detail = (e as CustomEvent).detail;
|
||||||
if (detail?.teamName === teamName) {
|
if (detail?.teamName === teamName) {
|
||||||
setGraphOpen((prev) => !prev);
|
useStore.getState().openTab({
|
||||||
|
type: 'graph',
|
||||||
|
label: `${teamName} Graph`,
|
||||||
|
teamName,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener('toggle-team-graph', handler);
|
window.addEventListener('toggle-team-graph', handler);
|
||||||
return () => window.removeEventListener('toggle-team-graph', handler);
|
return () => window.removeEventListener('toggle-team-graph', handler);
|
||||||
}, [teamName]);
|
}, [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 [sendDialogOpen, setSendDialogOpen] = useState(false);
|
||||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||||
const [stoppingTeam, setStoppingTeam] = 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)]"
|
className="h-6 gap-1 px-2 text-xs text-[var(--color-text-muted)] hover:text-[var(--color-text)]"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setGraphOpen(true);
|
useStore.getState().openTab({
|
||||||
|
type: 'graph',
|
||||||
|
label: `${data.config.name} Graph`,
|
||||||
|
teamName,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Network size={12} />
|
<Network size={12} />
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
/**
|
/**
|
||||||
* TeamGraphTab — wraps GraphView for use as a dedicated tab.
|
* 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';
|
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';
|
import type { GraphDomainRef, GraphEventPort } from '@claude-teams/agent-graph';
|
||||||
|
|
||||||
|
const TeamGraphOverlay = lazy(() =>
|
||||||
|
import('./TeamGraphOverlay').then((m) => ({ default: m.TeamGraphOverlay }))
|
||||||
|
);
|
||||||
|
|
||||||
export interface TeamGraphTabProps {
|
export interface TeamGraphTabProps {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TeamGraphTab = ({ teamName }: TeamGraphTabProps): React.JSX.Element => {
|
export const TeamGraphTab = ({ teamName }: TeamGraphTabProps): React.JSX.Element => {
|
||||||
const graphData = useTeamGraphAdapter(teamName);
|
const graphData = useTeamGraphAdapter(teamName);
|
||||||
|
const [fullscreen, setFullscreen] = useState(false);
|
||||||
|
|
||||||
const events: GraphEventPort = {
|
const events: GraphEventPort = {
|
||||||
onNodeDoubleClick: useCallback((ref: GraphDomainRef) => {
|
onNodeDoubleClick: useCallback(
|
||||||
console.log('Double-click in tab:', ref);
|
(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 (
|
return (
|
||||||
<div className="size-full" style={{ background: '#050510' }}>
|
<div className="size-full" style={{ background: '#050510' }}>
|
||||||
<GraphView data={graphData} events={events} className="size-full" />
|
<GraphView
|
||||||
|
data={graphData}
|
||||||
|
events={events}
|
||||||
|
className="size-full"
|
||||||
|
onRequestFullscreen={() => setFullscreen(true)}
|
||||||
|
/>
|
||||||
|
{fullscreen && (
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<TeamGraphOverlay teamName={teamName} onClose={() => setFullscreen(false)} />
|
||||||
|
</Suspense>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue