detached 42ae348

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
625625 Parent: stackNode,
626626 }
627627
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
631644 stackNode.ActiveStackIndex = newIndex
632645
633646 // 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 stack628+ // 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) - 1630+ 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 = newIndex644 stackNode.ActiveStackIndex = newIndex
632 645
633 // Update workspace active pane646 // Update workspace active pane
M internal/ui/coordinator/workspace.go +44 -7
@@ -1026,8 +1026,16 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
10261026 // Update domain model: convert leaf to stacked if needed, add new pane
10271027 var stackNode *entity.PaneNode
10281028 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
10311039 stackNode = activeNode
10321040 } else {
10331041 // Convert leaf node to stacked container
@@ -1058,13 +1066,28 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error {
10581066 Pane: newPane,
10591067 Parent: stackNode,
10601068 }
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
10631085
10641086 log.Debug().
10651087 Int("stack_size", len(stackNode.Children)).
1088+ Int("insert_index", insertIndex).
10661089 Int("active_index", stackNode.ActiveStackIndex).
1067- Msg("domain tree updated")
1090+ Msg("domain tree updated with position-aware insertion")
10681091
10691092 // Create PaneView for the new pane
10701093 newPaneView := component.NewPaneView(c.widgetFactory, newPaneID, nil)
@@ -1458,8 +1481,22 @@ func (c *WorkspaceCoordinator) insertPopupStacked(ctx context.Context, input Ins
14581481 Pane: input.PopupPane,
14591482 Parent: stackNode,
14601483 }
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
14631500
14641501 // Update workspace active pane
14651502 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 pane1026 // Update domain model: convert leaf to stacked if needed, add new pane
1027 var stackNode *entity.PaneNode1027 var stackNode *entity.PaneNode
1028 var needsFirstPaneTitleUpdate bool1028 var needsFirstPaneTitleUpdate bool
1029- if activeNode.IsStacked {1029+ if activeNode.Parent != nil && activeNode.Parent.IsStacked {
1030- // Already stacked, just add to it1030+ // 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 = activeNode1039 stackNode = activeNode
1032 } else {1040 } else {
1033 // Convert leaf node to stacked container1041 // 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) - 11070+ // 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 pane1092 // 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) - 11485+ // 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 pane1501 // Update workspace active pane
1465 ws.ActivePaneID = input.PopupPane.ID1502 ws.ActivePaneID = input.PopupPane.ID

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close