-
Notifications
You must be signed in to change notification settings - Fork 2
Improve workspace graph hierarchy, cards, and performance #149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
d01d7bb
7dd6967
d6c9370
0cafefa
4fdc169
34472f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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<string | null>(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<string, string>(); | ||
| 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<Node<GraphNodeData>> = []; | ||
|
|
@@ -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<Edge<GraphEdgeData>> = []; | ||
| const primaryLane = lanes.find((lane) => lane.laneType === "primary") ?? null; | ||
| const primaryLane = primaryHierarchyMeta.primary; | ||
| const riskPairsWithVisibleEdge = new Set<string>(); | ||
| 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)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [🟡 Medium] [🔵 Bug] This new render gate no longer matches the earlier // apps/desktop/src/renderer/components/graph/WorkspaceGraphPage.tsx
if (viewMode === "risk" || (viewMode === "all" && showOverviewRiskEdges)) {
for (const [key, risk] of riskByPair.entries()) {
if (risk.riskLevel === "none" && risk.overlapCount === 0) continue; |
||
| 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 | ||
| </Button> | ||
|
|
||
| {viewMode === "all" ? ( | ||
| <Button | ||
| size="sm" | ||
| variant={showOverviewRiskEdges ? "primary" : "outline"} | ||
| className="h-7 px-2 text-[11px]" | ||
| title="Show or hide predicted file-overlap links between lanes. Stack links stay visible." | ||
| onClick={() => setShowOverviewRiskEdges((prev) => !prev)} | ||
| > | ||
| {showOverviewRiskEdges ? "Hide overlap web" : "Show overlap web"} | ||
| </Button> | ||
| ) : null} | ||
|
|
||
| <Button | ||
| size="sm" | ||
| variant={showRiskMatrix ? "primary" : "outline"} | ||
|
|
@@ -2940,6 +2974,8 @@ function GraphInner() { | |
| edges={edges} | ||
| nodeTypes={nodeTypes} | ||
| edgeTypes={edgeTypes} | ||
| onlyRenderVisibleElements | ||
| nodesConnectable={false} | ||
| onNodesChange={onNodesChange} | ||
| onEdgesChange={onEdgesChange} | ||
| onNodeDragStart={onNodeDragStart} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[🟠 High] [🔵 Bug]
This memo now assumes
laneHierarchyFromPrimary(lanes)always returns a real primary lane, butGraphInnerlater has an explicitif (lanes.length === 0)empty-state branch and the app store initializeslanesas[]. In the empty-workspace / initial-load case,laneHierarchyFromPrimaryfalls back tolanes[0]!, soprimaryHierarchyMeta.primary.idthrows before the component can render either the loading UI or the "No lanes yet" state. Guard the hierarchy helper for empty input or makeprimarynullable and handle that here before dereferencing.id.