fix(pane-stacking): check parent stack before adding nested stacks
When stacking a pane that is already in a stack, check if the active node's parent is a stack before checking if the node itself is stacked. This prevents creating nested stacks and correctly identifies the target stack for insertion.
- commit
- 42ae348bfc6f5c94752f90cb2f8996b229bdefd0
- author
- brice · · 2025-12-17 21:32 UTC
- committer
- brice · · 2025-12-17 17:49 UTC
- parent
- 5740d12
- signature
- unsigned
2 files · +60 -10
M internal/application/usecase/manage_panes.go +16 -3
| @@ -625,9 +625,22 @@ func (uc *ManagePanesUseCase) AddToStack(ctx context.Context, ws *entity.Workspa | |||
| 625 | 625 | Parent: stackNode, | |
| 626 | 626 | } | |
| 627 | 627 | ||
| 628 | - | // Add to stack | |
| 629 | - | stackNode.Children = append(stackNode.Children, newNode) | |
| 630 | - | newIndex := len(stackNode.Children) - 1 | |
| 628 | + | // Insert right after current active pane (not at end) | |
| 629 | + | currentActiveIndex := stackNode.ActiveStackIndex | |
| 630 | + | if currentActiveIndex < 0 || currentActiveIndex >= len(stackNode.Children) { | |
| 631 | + | // Clamp to valid range - default to end-1 if invalid | |
| 632 | + | currentActiveIndex = len(stackNode.Children) - 1 | |
| 633 | + | if currentActiveIndex < 0 { | |
| 634 | + | currentActiveIndex = -1 // Will result in insertIndex = 0 | |
| 635 | + | } | |
| 636 | + | } | |
| 637 | + | insertIndex := currentActiveIndex + 1 | |
| 638 | + | ||
| 639 | + | // Slice insertion at correct position | |
| 640 | + | stackNode.Children = append(stackNode.Children, nil) | |
| 641 | + | copy(stackNode.Children[insertIndex+1:], stackNode.Children[insertIndex:]) | |
| 642 | + | stackNode.Children[insertIndex] = newNode | |
| 643 | + | newIndex := insertIndex | |
| 631 | 644 | stackNode.ActiveStackIndex = newIndex | |
| 632 | 645 | ||
| 633 | 646 | // Update workspace active pane | |
| @@ -625,9 +625,22 @@ func (uc *ManagePanesUseCase) AddToStack(ctx context.Context, ws *entity.Workspa | |||||
| 625 | Parent: stackNode, | 625 | Parent: stackNode, | ||
| 626 | } | 626 | } | ||
| 627 | 627 | ||||
| 628 | - | // Add to stack | 628 | + | // Insert right after current active pane (not at end) |
| 629 | - | stackNode.Children = append(stackNode.Children, newNode) | 629 | + | currentActiveIndex := stackNode.ActiveStackIndex |
| 630 | - | newIndex := len(stackNode.Children) - 1 | 630 | + | if currentActiveIndex < 0 || currentActiveIndex >= len(stackNode.Children) { |
| 631 | + | // Clamp to valid range - default to end-1 if invalid | |||
| 632 | + | currentActiveIndex = len(stackNode.Children) - 1 | |||
| 633 | + | if currentActiveIndex < 0 { | |||
| 634 | + | currentActiveIndex = -1 // Will result in insertIndex = 0 | |||
| 635 | + | } | |||
| 636 | + | } | |||
| 637 | + | insertIndex := currentActiveIndex + 1 | |||
| 638 | + | ||||
| 639 | + | // Slice insertion at correct position | |||
| 640 | + | stackNode.Children = append(stackNode.Children, nil) | |||
| 641 | + | copy(stackNode.Children[insertIndex+1:], stackNode.Children[insertIndex:]) | |||
| 642 | + | stackNode.Children[insertIndex] = newNode | |||
| 643 | + | newIndex := insertIndex | |||
| 631 | stackNode.ActiveStackIndex = newIndex | 644 | stackNode.ActiveStackIndex = newIndex | ||
| 632 | 645 | ||||
| 633 | // Update workspace active pane | 646 | // Update workspace active pane | ||
M internal/ui/coordinator/workspace.go +44 -7
| @@ -1026,8 +1026,16 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error { | |||
| 1026 | 1026 | // Update domain model: convert leaf to stacked if needed, add new pane | |
| 1027 | 1027 | var stackNode *entity.PaneNode | |
| 1028 | 1028 | var needsFirstPaneTitleUpdate bool | |
| 1029 | - | if activeNode.IsStacked { | |
| 1030 | - | // Already stacked, just add to it | |
| 1029 | + | if activeNode.Parent != nil && activeNode.Parent.IsStacked { | |
| 1030 | + | // Active pane is already in a stack - add to parent stack | |
| 1031 | + | stackNode = activeNode.Parent | |
| 1032 | + | log.Debug(). | |
| 1033 | + | Str("stack_id", stackNode.ID). | |
| 1034 | + | Str("active_pane", string(activePaneID)). | |
| 1035 | + | Int("stack_size", len(stackNode.Children)). | |
| 1036 | + | Msg("adding to existing stack via parent") | |
| 1037 | + | } else if activeNode.IsStacked { | |
| 1038 | + | // Active node is itself a stack container - add to it | |
| 1031 | 1039 | stackNode = activeNode | |
| 1032 | 1040 | } else { | |
| 1033 | 1041 | // Convert leaf node to stacked container | |
| @@ -1058,13 +1066,28 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error { | |||
| 1058 | 1066 | Pane: newPane, | |
| 1059 | 1067 | Parent: stackNode, | |
| 1060 | 1068 | } | |
| 1061 | - | stackNode.Children = append(stackNode.Children, newChildNode) | |
| 1062 | - | stackNode.ActiveStackIndex = len(stackNode.Children) - 1 | |
| 1069 | + | ||
| 1070 | + | // Insert right after current active pane (not at end) | |
| 1071 | + | currentActiveIndex := stackNode.ActiveStackIndex | |
| 1072 | + | if currentActiveIndex < 0 || currentActiveIndex >= len(stackNode.Children) { | |
| 1073 | + | currentActiveIndex = len(stackNode.Children) - 1 | |
| 1074 | + | if currentActiveIndex < 0 { | |
| 1075 | + | currentActiveIndex = -1 | |
| 1076 | + | } | |
| 1077 | + | } | |
| 1078 | + | insertIndex := currentActiveIndex + 1 | |
| 1079 | + | ||
| 1080 | + | // Slice insertion at correct position | |
| 1081 | + | stackNode.Children = append(stackNode.Children, nil) | |
| 1082 | + | copy(stackNode.Children[insertIndex+1:], stackNode.Children[insertIndex:]) | |
| 1083 | + | stackNode.Children[insertIndex] = newChildNode | |
| 1084 | + | stackNode.ActiveStackIndex = insertIndex | |
| 1063 | 1085 | ||
| 1064 | 1086 | log.Debug(). | |
| 1065 | 1087 | Int("stack_size", len(stackNode.Children)). | |
| 1088 | + | Int("insert_index", insertIndex). | |
| 1066 | 1089 | Int("active_index", stackNode.ActiveStackIndex). | |
| 1067 | - | Msg("domain tree updated") | |
| 1090 | + | Msg("domain tree updated with position-aware insertion") | |
| 1068 | 1091 | ||
| 1069 | 1092 | // Create PaneView for the new pane | |
| 1070 | 1093 | newPaneView := component.NewPaneView(c.widgetFactory, newPaneID, nil) | |
| @@ -1458,8 +1481,22 @@ func (c *WorkspaceCoordinator) insertPopupStacked(ctx context.Context, input Ins | |||
| 1458 | 1481 | Pane: input.PopupPane, | |
| 1459 | 1482 | Parent: stackNode, | |
| 1460 | 1483 | } | |
| 1461 | - | stackNode.Children = append(stackNode.Children, newChildNode) | |
| 1462 | - | stackNode.ActiveStackIndex = len(stackNode.Children) - 1 | |
| 1484 | + | ||
| 1485 | + | // Insert right after current active pane (not at end) | |
| 1486 | + | currentActiveIndex := stackNode.ActiveStackIndex | |
| 1487 | + | if currentActiveIndex < 0 || currentActiveIndex >= len(stackNode.Children) { | |
| 1488 | + | currentActiveIndex = len(stackNode.Children) - 1 | |
| 1489 | + | if currentActiveIndex < 0 { | |
| 1490 | + | currentActiveIndex = -1 | |
| 1491 | + | } | |
| 1492 | + | } | |
| 1493 | + | insertIndex := currentActiveIndex + 1 | |
| 1494 | + | ||
| 1495 | + | // Slice insertion at correct position | |
| 1496 | + | stackNode.Children = append(stackNode.Children, nil) | |
| 1497 | + | copy(stackNode.Children[insertIndex+1:], stackNode.Children[insertIndex:]) | |
| 1498 | + | stackNode.Children[insertIndex] = newChildNode | |
| 1499 | + | stackNode.ActiveStackIndex = insertIndex | |
| 1463 | 1500 | ||
| 1464 | 1501 | // Update workspace active pane | |
| 1465 | 1502 | ws.ActivePaneID = input.PopupPane.ID | |
| @@ -1026,8 +1026,16 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error { | |||||
| 1026 | // Update domain model: convert leaf to stacked if needed, add new pane | 1026 | // Update domain model: convert leaf to stacked if needed, add new pane | ||
| 1027 | var stackNode *entity.PaneNode | 1027 | var stackNode *entity.PaneNode | ||
| 1028 | var needsFirstPaneTitleUpdate bool | 1028 | var needsFirstPaneTitleUpdate bool | ||
| 1029 | - | if activeNode.IsStacked { | 1029 | + | if activeNode.Parent != nil && activeNode.Parent.IsStacked { |
| 1030 | - | // Already stacked, just add to it | 1030 | + | // Active pane is already in a stack - add to parent stack |
| 1031 | + | stackNode = activeNode.Parent | |||
| 1032 | + | log.Debug(). | |||
| 1033 | + | Str("stack_id", stackNode.ID). | |||
| 1034 | + | Str("active_pane", string(activePaneID)). | |||
| 1035 | + | Int("stack_size", len(stackNode.Children)). | |||
| 1036 | + | Msg("adding to existing stack via parent") | |||
| 1037 | + | } else if activeNode.IsStacked { | |||
| 1038 | + | // Active node is itself a stack container - add to it | |||
| 1031 | stackNode = activeNode | 1039 | stackNode = activeNode | ||
| 1032 | } else { | 1040 | } else { | ||
| 1033 | // Convert leaf node to stacked container | 1041 | // Convert leaf node to stacked container | ||
| @@ -1058,13 +1066,28 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error { | |||||
| 1058 | Pane: newPane, | 1066 | Pane: newPane, | ||
| 1059 | Parent: stackNode, | 1067 | Parent: stackNode, | ||
| 1060 | } | 1068 | } | ||
| 1061 | - | stackNode.Children = append(stackNode.Children, newChildNode) | 1069 | + | |
| 1062 | - | stackNode.ActiveStackIndex = len(stackNode.Children) - 1 | 1070 | + | // Insert right after current active pane (not at end) |
| 1071 | + | currentActiveIndex := stackNode.ActiveStackIndex | |||
| 1072 | + | if currentActiveIndex < 0 || currentActiveIndex >= len(stackNode.Children) { | |||
| 1073 | + | currentActiveIndex = len(stackNode.Children) - 1 | |||
| 1074 | + | if currentActiveIndex < 0 { | |||
| 1075 | + | currentActiveIndex = -1 | |||
| 1076 | + | } | |||
| 1077 | + | } | |||
| 1078 | + | insertIndex := currentActiveIndex + 1 | |||
| 1079 | + | ||||
| 1080 | + | // Slice insertion at correct position | |||
| 1081 | + | stackNode.Children = append(stackNode.Children, nil) | |||
| 1082 | + | copy(stackNode.Children[insertIndex+1:], stackNode.Children[insertIndex:]) | |||
| 1083 | + | stackNode.Children[insertIndex] = newChildNode | |||
| 1084 | + | stackNode.ActiveStackIndex = insertIndex | |||
| 1063 | 1085 | ||||
| 1064 | log.Debug(). | 1086 | log.Debug(). | ||
| 1065 | Int("stack_size", len(stackNode.Children)). | 1087 | Int("stack_size", len(stackNode.Children)). | ||
| 1088 | + | Int("insert_index", insertIndex). | |||
| 1066 | Int("active_index", stackNode.ActiveStackIndex). | 1089 | Int("active_index", stackNode.ActiveStackIndex). | ||
| 1067 | - | Msg("domain tree updated") | 1090 | + | Msg("domain tree updated with position-aware insertion") |
| 1068 | 1091 | ||||
| 1069 | // Create PaneView for the new pane | 1092 | // Create PaneView for the new pane | ||
| 1070 | newPaneView := component.NewPaneView(c.widgetFactory, newPaneID, nil) | 1093 | newPaneView := component.NewPaneView(c.widgetFactory, newPaneID, nil) | ||
| @@ -1458,8 +1481,22 @@ func (c *WorkspaceCoordinator) insertPopupStacked(ctx context.Context, input Ins | |||||
| 1458 | Pane: input.PopupPane, | 1481 | Pane: input.PopupPane, | ||
| 1459 | Parent: stackNode, | 1482 | Parent: stackNode, | ||
| 1460 | } | 1483 | } | ||
| 1461 | - | stackNode.Children = append(stackNode.Children, newChildNode) | 1484 | + | |
| 1462 | - | stackNode.ActiveStackIndex = len(stackNode.Children) - 1 | 1485 | + | // Insert right after current active pane (not at end) |
| 1486 | + | currentActiveIndex := stackNode.ActiveStackIndex | |||
| 1487 | + | if currentActiveIndex < 0 || currentActiveIndex >= len(stackNode.Children) { | |||
| 1488 | + | currentActiveIndex = len(stackNode.Children) - 1 | |||
| 1489 | + | if currentActiveIndex < 0 { | |||
| 1490 | + | currentActiveIndex = -1 | |||
| 1491 | + | } | |||
| 1492 | + | } | |||
| 1493 | + | insertIndex := currentActiveIndex + 1 | |||
| 1494 | + | ||||
| 1495 | + | // Slice insertion at correct position | |||
| 1496 | + | stackNode.Children = append(stackNode.Children, nil) | |||
| 1497 | + | copy(stackNode.Children[insertIndex+1:], stackNode.Children[insertIndex:]) | |||
| 1498 | + | stackNode.Children[insertIndex] = newChildNode | |||
| 1499 | + | stackNode.ActiveStackIndex = insertIndex | |||
| 1463 | 1500 | ||||
| 1464 | // Update workspace active pane | 1501 | // Update workspace active pane | ||
| 1465 | ws.ActivePaneID = input.PopupPane.ID | 1502 | ws.ActivePaneID = input.PopupPane.ID | ||