diff --git a/apps/desktop/src/renderer/components/graph/WorkspaceGraphPage.tsx b/apps/desktop/src/renderer/components/graph/WorkspaceGraphPage.tsx index be65588de..41f23f0ed 100644 --- a/apps/desktop/src/renderer/components/graph/WorkspaceGraphPage.tsx +++ b/apps/desktop/src/renderer/components/graph/WorkspaceGraphPage.tsx @@ -101,7 +101,8 @@ import { createGraphPreferences, createSessionState, normalizeGraphPreferences, - computeAutoLayout + computeAutoLayout, + laneHierarchyFromPrimary } from "./graphLayout"; import { GraphLaneNode } from "./graphNodes/LaneNode"; import { GraphProposalNode } from "./graphNodes/ProposalNode"; @@ -336,6 +337,12 @@ function GraphInner() { const [textPromptError, setTextPromptError] = React.useState(null); const [singleActionsOpen, setSingleActionsOpen] = React.useState(false); const [batchActionsOpen, setBatchActionsOpen] = React.useState(false); + /** In Overview, risk edges are hidden by default so the tree stays readable. */ + const [showOverviewRiskEdges, setShowOverviewRiskEdges] = React.useState(false); + + React.useEffect(() => { + if (viewMode !== "all") setShowOverviewRiskEdges(false); + }, [viewMode]); React.useEffect(() => { void refreshEnvironmentMappings(); @@ -463,7 +470,8 @@ function GraphInner() { }, [collapsedLaneIds, lanes]); const laneById = React.useMemo(() => new Map(lanes.map((lane) => [lane.id, lane] as const)), [lanes]); - const primaryLaneId = React.useMemo(() => lanes.find((lane) => lane.laneType === "primary")?.id ?? null, [lanes]); + const primaryHierarchyMeta = React.useMemo(() => laneHierarchyFromPrimary(lanes), [lanes]); + const primaryLaneId = primaryHierarchyMeta.primary?.id ?? null; const laneIdByBranchRef = React.useMemo(() => { const map = new Map(); for (const lane of lanes) { @@ -514,7 +522,7 @@ function GraphInner() { (laneId: string): string | null => { const lane = laneById.get(laneId); if (!lane) return null; - return resolvePrBaseLaneId(lane, lane.baseRef); + return resolvePrBaseLaneId(lane, lane.baseRef) ?? null; }, [laneById, resolvePrBaseLaneId] ); @@ -1135,7 +1143,13 @@ function GraphInner() { }; } - const autoPositions = computeAutoLayout(lanes, viewMode, activityScoreByLaneId, environmentByLaneId); + const autoPositions = computeAutoLayout( + lanes, + viewMode, + activityScoreByLaneId, + environmentByLaneId, + primaryHierarchyMeta.depthByLaneId + ); const savedPositions = activeSnapshot.nodePositions; const positions = Object.keys(savedPositions).length > 0 ? { ...autoPositions, ...savedPositions } : autoPositions; const nextNodes: Array> = []; @@ -1166,6 +1180,8 @@ function GraphInner() { autoRebaseStatus: autoRebaseByLaneId[lane.id] ?? null, activeSessions: activeSessionsByLaneId[lane.id] ?? 0, collapsedChildCount, + hierarchyDepth: primaryHierarchyMeta.depthByLaneId.get(lane.id) ?? 0, + parentLaneName: primaryHierarchyMeta.parentNameByLaneId.get(lane.id) ?? null, dimmed: false, activityBucket: activityBucketByLaneId[lane.id] ?? "medium", viewMode, @@ -1246,6 +1262,8 @@ function GraphInner() { autoRebaseStatus: null, activeSessions: 0, collapsedChildCount: 0, + hierarchyDepth: 0, + parentLaneName: null, dimmed: false, activityBucket: "medium", viewMode, @@ -1274,7 +1292,7 @@ function GraphInner() { } const nextEdges: Array> = []; - const primaryLane = lanes.find((lane) => lane.laneType === "primary") ?? null; + const primaryLane = primaryHierarchyMeta.primary; const riskPairsWithVisibleEdge = new Set(); const laneHasProposalConflict = (proposal: IntegrationProposal, sourceLaneId: string): boolean => { const steps = proposalSteps(proposal); @@ -1292,7 +1310,7 @@ function GraphInner() { ); }; - if (viewMode === "all" || viewMode === "risk") { + if (viewMode === "risk" || (viewMode === "all" && showOverviewRiskEdges)) { for (const [key, risk] of riskByPair.entries()) { if (risk.riskLevel === "none" && risk.overlapCount === 0) continue; const [laneAId, laneBId] = key.split("::"); @@ -1306,6 +1324,8 @@ function GraphInner() { if (viewMode === "all" || viewMode === "stack") { for (const lane of lanes) { if (!primaryLane || lane.id === primaryLane.id) continue; + // In Overview, stack edges already show the tree; skip redundant "primary → lane" spokes. + if (viewMode === "all" && lane.parentLaneId && visibleNodeIds.has(lane.parentLaneId)) continue; if (!visibleNodeIds.has(primaryLane.id) || !visibleNodeIds.has(lane.id)) continue; const pair = edgePairKey(primaryLane.id, lane.id); const pr = prOverlayByPair.get(pair); @@ -1341,7 +1361,7 @@ function GraphInner() { } } - if (viewMode === "all" || viewMode === "risk") { + if (viewMode === "risk" || (viewMode === "all" && showOverviewRiskEdges)) { for (const [key, risk] of riskByPair.entries()) { if (risk.riskLevel === "none" && risk.overlapCount === 0) continue; const [laneAId, laneBId] = key.split("::"); @@ -1433,7 +1453,9 @@ function GraphInner() { loadedGraphPreferences, prOverlayByPair, prOverlayByLaneId, + primaryHierarchyMeta, riskByPair, + showOverviewRiskEdges, statusByLane, syncByLaneId, viewMode, @@ -2922,6 +2944,18 @@ function GraphInner() { Reset View + {viewMode === "all" ? ( + + ) : null} +