Repository navigation
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 2 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,11 @@ 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 = React.useMemo( | ||
| () => primaryHierarchyMeta.primary.id, | ||
| [primaryHierarchyMeta.primary.id] | ||
| ); | ||
|
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. Guard hierarchy metadata while lanes are empty. This hook runs before the Proposed guard- const primaryHierarchyMeta = React.useMemo(() => laneHierarchyFromPrimary(lanes), [lanes]);
- const primaryLaneId = React.useMemo(
- () => primaryHierarchyMeta.primary.id,
- [primaryHierarchyMeta.primary.id]
- );
+ const primaryHierarchyMeta = React.useMemo(
+ () => (lanes.length > 0 ? laneHierarchyFromPrimary(lanes) : null),
+ [lanes]
+ );
+ const primaryLaneId = primaryHierarchyMeta?.primary.id ?? null;Then update downstream reads to handle the null case, e.g. 🤖 Prompt for AI Agents |
||
| const laneIdByBranchRef = React.useMemo(() => { | ||
| const map = new Map<string, string>(); | ||
| for (const lane of lanes) { | ||
|
|
@@ -1166,6 +1177,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 +1259,8 @@ function GraphInner() { | |
| autoRebaseStatus: null, | ||
| activeSessions: 0, | ||
| collapsedChildCount: 0, | ||
| hierarchyDepth: 0, | ||
| parentLaneName: null, | ||
| dimmed: false, | ||
| activityBucket: "medium", | ||
| viewMode, | ||
|
|
@@ -1274,7 +1289,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); | ||
|
|
@@ -1306,6 +1321,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 && laneById.has(lane.parentLaneId)) continue; | ||
|
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] The new Overview spoke suppression only checks whether the lane has an in-workspace parent, not whether that parent is actually visible. ```ts |
||
| if (!visibleNodeIds.has(primaryLane.id) || !visibleNodeIds.has(lane.id)) continue; | ||
| const pair = edgePairKey(primaryLane.id, lane.id); | ||
| const pr = prOverlayByPair.get(pair); | ||
|
|
@@ -1341,7 +1358,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 +1450,9 @@ function GraphInner() { | |
| loadedGraphPreferences, | ||
| prOverlayByPair, | ||
| prOverlayByLaneId, | ||
| primaryHierarchyMeta, | ||
| riskByPair, | ||
| showOverviewRiskEdges, | ||
| statusByLane, | ||
| syncByLaneId, | ||
| viewMode, | ||
|
|
@@ -2922,6 +2941,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 +2971,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.