refactor(stack): use cases for domain model sync in stacked panes
- commit
- 172cc2ccc925e754b27bdc8f55c921e8569a79f0
- author
- bnema · · 2026-01-07 19:02 UTC
- parent
- e9e5ddc
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +275 -126
M internal/application/usecase/manage_panes.go +95 -1
| @@ -455,6 +455,7 @@ func clampFloat64(v, minVal, maxVal float64) float64 { | |||
| 455 | 455 | ||
| 456 | 456 | // Close removes a pane and promotes its sibling. | |
| 457 | 457 | // Returns the sibling node that was promoted, or nil if this was the last pane. | |
| 458 | + | // For stacked panes, removes the pane from the stack via RemoveFromStack. | |
| 458 | 459 | func (uc *ManagePanesUseCase) Close(ctx context.Context, ws *entity.Workspace, paneNode *entity.PaneNode) (*entity.PaneNode, error) { | |
| 459 | 460 | log := logging.FromContext(ctx) | |
| 460 | 461 | if uc == nil { | |
| @@ -482,7 +483,12 @@ func (uc *ManagePanesUseCase) Close(ctx context.Context, ws *entity.Workspace, p | |||
| 482 | 483 | return nil, nil | |
| 483 | 484 | } | |
| 484 | 485 | ||
| 485 | - | // Find sibling | |
| 486 | + | // Handle stacked panes differently - they can have more than 2 children | |
| 487 | + | if parent.IsStacked { | |
| 488 | + | return uc.closeStackedPane(ctx, ws, paneNode, parent) | |
| 489 | + | } | |
| 490 | + | ||
| 491 | + | // Binary split: find sibling (the other child of parent) | |
| 486 | 492 | var sibling *entity.PaneNode | |
| 487 | 493 | for _, child := range parent.Children { | |
| 488 | 494 | if child != paneNode { | |
| @@ -539,6 +545,94 @@ func (uc *ManagePanesUseCase) Close(ctx context.Context, ws *entity.Workspace, p | |||
| 539 | 545 | return sibling, nil | |
| 540 | 546 | } | |
| 541 | 547 | ||
| 548 | + | // closeStackedPane handles closing a pane within a stack. | |
| 549 | + | // If multiple panes remain, removes the pane from the stack. | |
| 550 | + | // If only one pane would remain, dissolves the stack. | |
| 551 | + | func (uc *ManagePanesUseCase) closeStackedPane( | |
| 552 | + | ctx context.Context, | |
| 553 | + | ws *entity.Workspace, | |
| 554 | + | paneNode *entity.PaneNode, | |
| 555 | + | stackNode *entity.PaneNode, | |
| 556 | + | ) (*entity.PaneNode, error) { | |
| 557 | + | log := logging.FromContext(ctx) | |
| 558 | + | ||
| 559 | + | paneID := paneNode.Pane.ID | |
| 560 | + | ||
| 561 | + | // If more than 2 panes in stack, just remove this one | |
| 562 | + | if len(stackNode.Children) > 2 { | |
| 563 | + | if err := uc.RemoveFromStack(ctx, stackNode, paneID); err != nil { | |
| 564 | + | return nil, fmt.Errorf("failed to remove pane from stack: %w", err) | |
| 565 | + | } | |
| 566 | + | ||
| 567 | + | // Update active pane if we closed the active one | |
| 568 | + | if ws.ActivePaneID == paneID { | |
| 569 | + | // Use the pane at the current active index | |
| 570 | + | if stackNode.ActiveStackIndex >= 0 && stackNode.ActiveStackIndex < len(stackNode.Children) { | |
| 571 | + | activeChild := stackNode.Children[stackNode.ActiveStackIndex] | |
| 572 | + | if activeChild.Pane != nil { | |
| 573 | + | ws.ActivePaneID = activeChild.Pane.ID | |
| 574 | + | } | |
| 575 | + | } | |
| 576 | + | } | |
| 577 | + | ||
| 578 | + | log.Info(). | |
| 579 | + | Str("pane_id", string(paneID)). | |
| 580 | + | Int("remaining", len(stackNode.Children)). | |
| 581 | + | Msg("pane removed from stack") | |
| 582 | + | ||
| 583 | + | return stackNode, nil | |
| 584 | + | } | |
| 585 | + | ||
| 586 | + | // If exactly 2 panes, remove this one and dissolve the stack | |
| 587 | + | if len(stackNode.Children) == 2 { | |
| 588 | + | // Find the remaining pane | |
| 589 | + | var remaining *entity.PaneNode | |
| 590 | + | for _, child := range stackNode.Children { | |
| 591 | + | if child.Pane != nil && child.Pane.ID != paneID { | |
| 592 | + | remaining = child | |
| 593 | + | break | |
| 594 | + | } | |
| 595 | + | } | |
| 596 | + | ||
| 597 | + | if remaining == nil { | |
| 598 | + | return nil, fmt.Errorf("no remaining pane found in stack") | |
| 599 | + | } | |
| 600 | + | ||
| 601 | + | // Dissolve the stack: promote remaining pane to stack's position | |
| 602 | + | grandparent := stackNode.Parent | |
| 603 | + | ||
| 604 | + | if grandparent == nil { | |
| 605 | + | // Stack was root, remaining becomes new root | |
| 606 | + | ws.Root = remaining | |
| 607 | + | remaining.Parent = nil | |
| 608 | + | } else { | |
| 609 | + | // Replace stack with remaining in grandparent | |
| 610 | + | for i, child := range grandparent.Children { | |
| 611 | + | if child == stackNode { | |
| 612 | + | grandparent.Children[i] = remaining | |
| 613 | + | break | |
| 614 | + | } | |
| 615 | + | } | |
| 616 | + | remaining.Parent = grandparent | |
| 617 | + | } | |
| 618 | + | ||
| 619 | + | // Update active pane if we closed the active one | |
| 620 | + | if ws.ActivePaneID == paneID && remaining.Pane != nil { | |
| 621 | + | ws.ActivePaneID = remaining.Pane.ID | |
| 622 | + | } | |
| 623 | + | ||
| 624 | + | log.Info(). | |
| 625 | + | Str("closed_pane_id", string(paneID)). | |
| 626 | + | Str("remaining_pane_id", remaining.ID). | |
| 627 | + | Msg("stack dissolved, remaining pane promoted") | |
| 628 | + | ||
| 629 | + | return remaining, nil | |
| 630 | + | } | |
| 631 | + | ||
| 632 | + | // Should not happen: stack with less than 2 children | |
| 633 | + | return nil, fmt.Errorf("invalid stack: has %d children", len(stackNode.Children)) | |
| 634 | + | } | |
| 635 | + | ||
| 542 | 636 | // Focus sets the active pane in the workspace. | |
| 543 | 637 | // Delegates to ApplyFocusChange to ensure stack index is updated when focusing a stacked pane. | |
| 544 | 638 | func (uc *ManagePanesUseCase) Focus(ctx context.Context, ws *entity.Workspace, paneID entity.PaneID) error { | |
| @@ -455,6 +455,7 @@ func clampFloat64(v, minVal, maxVal float64) float64 { | |||||
| 455 | 455 | ||||
| 456 | // Close removes a pane and promotes its sibling. | 456 | // Close removes a pane and promotes its sibling. | ||
| 457 | // Returns the sibling node that was promoted, or nil if this was the last pane. | 457 | // Returns the sibling node that was promoted, or nil if this was the last pane. | ||
| 458 | + | // For stacked panes, removes the pane from the stack via RemoveFromStack. | |||
| 458 | func (uc *ManagePanesUseCase) Close(ctx context.Context, ws *entity.Workspace, paneNode *entity.PaneNode) (*entity.PaneNode, error) { | 459 | func (uc *ManagePanesUseCase) Close(ctx context.Context, ws *entity.Workspace, paneNode *entity.PaneNode) (*entity.PaneNode, error) { | ||
| 459 | log := logging.FromContext(ctx) | 460 | log := logging.FromContext(ctx) | ||
| 460 | if uc == nil { | 461 | if uc == nil { | ||
| @@ -482,7 +483,12 @@ func (uc *ManagePanesUseCase) Close(ctx context.Context, ws *entity.Workspace, p | |||||
| 482 | return nil, nil | 483 | return nil, nil | ||
| 483 | } | 484 | } | ||
| 484 | 485 | ||||
| 485 | - | // Find sibling | 486 | + | // Handle stacked panes differently - they can have more than 2 children |
| 487 | + | if parent.IsStacked { | |||
| 488 | + | return uc.closeStackedPane(ctx, ws, paneNode, parent) | |||
| 489 | + | } | |||
| 490 | + | ||||
| 491 | + | // Binary split: find sibling (the other child of parent) | |||
| 486 | var sibling *entity.PaneNode | 492 | var sibling *entity.PaneNode | ||
| 487 | for _, child := range parent.Children { | 493 | for _, child := range parent.Children { | ||
| 488 | if child != paneNode { | 494 | if child != paneNode { | ||
| @@ -539,6 +545,94 @@ func (uc *ManagePanesUseCase) Close(ctx context.Context, ws *entity.Workspace, p | |||||
| 539 | return sibling, nil | 545 | return sibling, nil | ||
| 540 | } | 546 | } | ||
| 541 | 547 | ||||
| 548 | + | // closeStackedPane handles closing a pane within a stack. | |||
| 549 | + | // If multiple panes remain, removes the pane from the stack. | |||
| 550 | + | // If only one pane would remain, dissolves the stack. | |||
| 551 | + | func (uc *ManagePanesUseCase) closeStackedPane( | |||
| 552 | + | ctx context.Context, | |||
| 553 | + | ws *entity.Workspace, | |||
| 554 | + | paneNode *entity.PaneNode, | |||
| 555 | + | stackNode *entity.PaneNode, | |||
| 556 | + | ) (*entity.PaneNode, error) { | |||
| 557 | + | log := logging.FromContext(ctx) | |||
| 558 | + | ||||
| 559 | + | paneID := paneNode.Pane.ID | |||
| 560 | + | ||||
| 561 | + | // If more than 2 panes in stack, just remove this one | |||
| 562 | + | if len(stackNode.Children) > 2 { | |||
| 563 | + | if err := uc.RemoveFromStack(ctx, stackNode, paneID); err != nil { | |||
| 564 | + | return nil, fmt.Errorf("failed to remove pane from stack: %w", err) | |||
| 565 | + | } | |||
| 566 | + | ||||
| 567 | + | // Update active pane if we closed the active one | |||
| 568 | + | if ws.ActivePaneID == paneID { | |||
| 569 | + | // Use the pane at the current active index | |||
| 570 | + | if stackNode.ActiveStackIndex >= 0 && stackNode.ActiveStackIndex < len(stackNode.Children) { | |||
| 571 | + | activeChild := stackNode.Children[stackNode.ActiveStackIndex] | |||
| 572 | + | if activeChild.Pane != nil { | |||
| 573 | + | ws.ActivePaneID = activeChild.Pane.ID | |||
| 574 | + | } | |||
| 575 | + | } | |||
| 576 | + | } | |||
| 577 | + | ||||
| 578 | + | log.Info(). | |||
| 579 | + | Str("pane_id", string(paneID)). | |||
| 580 | + | Int("remaining", len(stackNode.Children)). | |||
| 581 | + | Msg("pane removed from stack") | |||
| 582 | + | ||||
| 583 | + | return stackNode, nil | |||
| 584 | + | } | |||
| 585 | + | ||||
| 586 | + | // If exactly 2 panes, remove this one and dissolve the stack | |||
| 587 | + | if len(stackNode.Children) == 2 { | |||
| 588 | + | // Find the remaining pane | |||
| 589 | + | var remaining *entity.PaneNode | |||
| 590 | + | for _, child := range stackNode.Children { | |||
| 591 | + | if child.Pane != nil && child.Pane.ID != paneID { | |||
| 592 | + | remaining = child | |||
| 593 | + | break | |||
| 594 | + | } | |||
| 595 | + | } | |||
| 596 | + | ||||
| 597 | + | if remaining == nil { | |||
| 598 | + | return nil, fmt.Errorf("no remaining pane found in stack") | |||
| 599 | + | } | |||
| 600 | + | ||||
| 601 | + | // Dissolve the stack: promote remaining pane to stack's position | |||
| 602 | + | grandparent := stackNode.Parent | |||
| 603 | + | ||||
| 604 | + | if grandparent == nil { | |||
| 605 | + | // Stack was root, remaining becomes new root | |||
| 606 | + | ws.Root = remaining | |||
| 607 | + | remaining.Parent = nil | |||
| 608 | + | } else { | |||
| 609 | + | // Replace stack with remaining in grandparent | |||
| 610 | + | for i, child := range grandparent.Children { | |||
| 611 | + | if child == stackNode { | |||
| 612 | + | grandparent.Children[i] = remaining | |||
| 613 | + | break | |||
| 614 | + | } | |||
| 615 | + | } | |||
| 616 | + | remaining.Parent = grandparent | |||
| 617 | + | } | |||
| 618 | + | ||||
| 619 | + | // Update active pane if we closed the active one | |||
| 620 | + | if ws.ActivePaneID == paneID && remaining.Pane != nil { | |||
| 621 | + | ws.ActivePaneID = remaining.Pane.ID | |||
| 622 | + | } | |||
| 623 | + | ||||
| 624 | + | log.Info(). | |||
| 625 | + | Str("closed_pane_id", string(paneID)). | |||
| 626 | + | Str("remaining_pane_id", remaining.ID). | |||
| 627 | + | Msg("stack dissolved, remaining pane promoted") | |||
| 628 | + | ||||
| 629 | + | return remaining, nil | |||
| 630 | + | } | |||
| 631 | + | ||||
| 632 | + | // Should not happen: stack with less than 2 children | |||
| 633 | + | return nil, fmt.Errorf("invalid stack: has %d children", len(stackNode.Children)) | |||
| 634 | + | } | |||
| 635 | + | ||||
| 542 | // Focus sets the active pane in the workspace. | 636 | // Focus sets the active pane in the workspace. | ||
| 543 | // Delegates to ApplyFocusChange to ensure stack index is updated when focusing a stacked pane. | 637 | // Delegates to ApplyFocusChange to ensure stack index is updated when focusing a stacked pane. | ||
| 544 | func (uc *ManagePanesUseCase) Focus(ctx context.Context, ws *entity.Workspace, paneID entity.PaneID) error { | 638 | func (uc *ManagePanesUseCase) Focus(ctx context.Context, ws *entity.Workspace, paneID entity.PaneID) error { | ||
M internal/ui/app.go +3 -0
| @@ -928,6 +928,9 @@ func (a *App) rebuildAndAttachWorkspace(ctx context.Context, tabID entity.TabID, | |||
| 928 | 928 | return | |
| 929 | 929 | } | |
| 930 | 930 | a.contentCoord.AttachToWorkspace(ctx, tab.Workspace, wsView) | |
| 931 | + | if a.wsCoord != nil { | |
| 932 | + | a.wsCoord.SetupStackedPaneCallbacks(ctx, tab.Workspace, wsView) | |
| 933 | + | } | |
| 931 | 934 | } | |
| 932 | 935 | ||
| 933 | 936 | func (a *App) switchToTargetTabIfConfigured(ctx context.Context, out *usecase.MovePaneToTabOutput) { | |
| @@ -928,6 +928,9 @@ func (a *App) rebuildAndAttachWorkspace(ctx context.Context, tabID entity.TabID, | |||||
| 928 | return | 928 | return | ||
| 929 | } | 929 | } | ||
| 930 | a.contentCoord.AttachToWorkspace(ctx, tab.Workspace, wsView) | 930 | a.contentCoord.AttachToWorkspace(ctx, tab.Workspace, wsView) | ||
| 931 | + | if a.wsCoord != nil { | |||
| 932 | + | a.wsCoord.SetupStackedPaneCallbacks(ctx, tab.Workspace, wsView) | |||
| 933 | + | } | |||
| 931 | } | 934 | } | ||
| 932 | 935 | ||||
| 933 | func (a *App) switchToTargetTabIfConfigured(ctx context.Context, out *usecase.MovePaneToTabOutput) { | 936 | func (a *App) switchToTargetTabIfConfigured(ctx context.Context, out *usecase.MovePaneToTabOutput) { | ||
M internal/ui/coordinator/workspace.go +177 -125
| @@ -263,6 +263,7 @@ func (c *WorkspaceCoordinator) applySplitToView( | |||
| 263 | 263 | ||
| 264 | 264 | if needsAttach { | |
| 265 | 265 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | |
| 266 | + | c.SetupStackedPaneCallbacks(ctx, ws, wsView) | |
| 266 | 267 | } | |
| 267 | 268 | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | |
| 268 | 269 | log.Warn().Err(err).Msg("failed to set active pane in workspace view") | |
| @@ -1055,6 +1056,7 @@ func (c *WorkspaceCoordinator) finalizePaneClose( | |||
| 1055 | 1056 | } | |
| 1056 | 1057 | c.contentCoord.ReleaseWebView(ctx, paneID) | |
| 1057 | 1058 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | |
| 1059 | + | c.SetupStackedPaneCallbacks(ctx, ws, wsView) | |
| 1058 | 1060 | } | |
| 1059 | 1061 | } else { | |
| 1060 | 1062 | if err := wsView.Rebuild(ctx); err != nil { | |
| @@ -1062,6 +1064,7 @@ func (c *WorkspaceCoordinator) finalizePaneClose( | |||
| 1062 | 1064 | } | |
| 1063 | 1065 | c.contentCoord.ReleaseWebView(ctx, paneID) | |
| 1064 | 1066 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | |
| 1067 | + | c.SetupStackedPaneCallbacks(ctx, ws, wsView) | |
| 1065 | 1068 | } | |
| 1066 | 1069 | ||
| 1067 | 1070 | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | |
| @@ -1095,17 +1098,10 @@ func (c *WorkspaceCoordinator) doIncrementalStackClose( | |||
| 1095 | 1098 | return fmt.Errorf("stacked view not found for pane %s", closingPaneID) | |
| 1096 | 1099 | } | |
| 1097 | 1100 | ||
| 1098 | - | // Find the index of the closing pane in the stack | |
| 1099 | - | closingIndex := -1 | |
| 1100 | - | for i, child := range stackNode.Children { | |
| 1101 | - | if child.Pane != nil && child.Pane.ID == closingPaneID { | |
| 1102 | - | closingIndex = i | |
| 1103 | - | break | |
| 1104 | - | } | |
| 1105 | - | } | |
| 1106 | - | ||
| 1101 | + | // Find the index of the closing pane in the StackedView UI | |
| 1102 | + | closingIndex := stackedView.FindPaneIndex(string(closingPaneID)) | |
| 1107 | 1103 | if closingIndex < 0 { | |
| 1108 | - | return fmt.Errorf("pane %s not found in stack", closingPaneID) | |
| 1104 | + | return fmt.Errorf("pane %s not found in stacked view", closingPaneID) | |
| 1109 | 1105 | } | |
| 1110 | 1106 | ||
| 1111 | 1107 | // Remove the pane from the StackedView widget | |
| @@ -1120,12 +1116,48 @@ func (c *WorkspaceCoordinator) doIncrementalStackClose( | |||
| 1120 | 1116 | // Release the closing pane's webview | |
| 1121 | 1117 | c.contentCoord.ReleaseWebView(ctx, closingPaneID) | |
| 1122 | 1118 | ||
| 1119 | + | // Sync remaining title bars with current titles from ContentCoordinator. | |
| 1120 | + | // The domain model (stackNode.Children) has already been updated by the use case, | |
| 1121 | + | // so we iterate the remaining children and update their titles in the UI. | |
| 1122 | + | c.syncStackedPaneTitles(ctx, stackedView, stackNode) | |
| 1123 | + | ||
| 1123 | 1124 | log.Debug(). | |
| 1124 | 1125 | Int("removed_index", closingIndex). | |
| 1125 | 1126 | Msg("incremental stack close completed successfully") | |
| 1126 | 1127 | return nil | |
| 1127 | 1128 | } | |
| 1128 | 1129 | ||
| 1130 | + | // syncStackedPaneTitles updates all title bars in a StackedView with current titles. | |
| 1131 | + | func (c *WorkspaceCoordinator) syncStackedPaneTitles(ctx context.Context, stackedView *layout.StackedView, stackNode *entity.PaneNode) { | |
| 1132 | + | log := logging.FromContext(ctx) | |
| 1133 | + | ||
| 1134 | + | if stackedView == nil || stackNode == nil { | |
| 1135 | + | return | |
| 1136 | + | } | |
| 1137 | + | ||
| 1138 | + | for i, child := range stackNode.Children { | |
| 1139 | + | if child.Pane == nil { | |
| 1140 | + | continue | |
| 1141 | + | } | |
| 1142 | + | ||
| 1143 | + | title := c.contentCoord.GetTitle(child.Pane.ID) | |
| 1144 | + | if title == "" { | |
| 1145 | + | title = child.Pane.Title | |
| 1146 | + | } | |
| 1147 | + | if title == "" { | |
| 1148 | + | title = defaultPaneTitle | |
| 1149 | + | } | |
| 1150 | + | ||
| 1151 | + | if err := stackedView.UpdateTitle(i, title); err != nil { | |
| 1152 | + | log.Warn(). | |
| 1153 | + | Err(err). | |
| 1154 | + | Int("index", i). | |
| 1155 | + | Str("pane_id", string(child.Pane.ID)). | |
| 1156 | + | Msg("failed to sync title bar") | |
| 1157 | + | } | |
| 1158 | + | } | |
| 1159 | + | } | |
| 1160 | + | ||
| 1129 | 1161 | // FocusPane navigates focus to an adjacent pane. | |
| 1130 | 1162 | func (c *WorkspaceCoordinator) FocusPane(ctx context.Context, direction usecase.NavigateDirection) error { | |
| 1131 | 1163 | log := logging.FromContext(ctx) | |
| @@ -1228,6 +1260,7 @@ func (c *WorkspaceCoordinator) syncStackedViewActive(ctx context.Context, wsView | |||
| 1228 | 1260 | } | |
| 1229 | 1261 | ||
| 1230 | 1262 | // StackPane adds a new pane stacked on top of the active pane. | |
| 1263 | + | // Uses CreateStack use case for new stacks, AddToStack for existing stacks. | |
| 1231 | 1264 | func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error { | |
| 1232 | 1265 | log := logging.FromContext(ctx) | |
| 1233 | 1266 | ||
| @@ -1236,19 +1269,64 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error { | |||
| 1236 | 1269 | return nil | |
| 1237 | 1270 | } | |
| 1238 | 1271 | ||
| 1272 | + | // Create new pane entity | |
| 1239 | 1273 | newPaneID := entity.PaneID(c.generateID()) | |
| 1240 | 1274 | newPane := entity.NewPane(newPaneID) | |
| 1241 | 1275 | newPane.URI = domainurl.Normalize(config.Get().Workspace.NewPaneURL) | |
| 1242 | 1276 | newPane.Title = defaultPaneTitle | |
| 1243 | 1277 | ||
| 1244 | - | stackNode, needsFirstPaneTitleUpdate := c.ensureStackNode(ctx, stackCtx.activeNode, stackCtx.activePaneID, stackCtx.originalTitle) | |
| 1245 | - | insertIndex := insertStackChild(stackNode, newPaneID, newPane) | |
| 1278 | + | // Determine if we need to create a new stack or add to existing | |
| 1279 | + | var stackNode *entity.PaneNode | |
| 1280 | + | var needsFirstPaneTitleUpdate bool | |
| 1246 | 1281 | ||
| 1247 | - | log.Debug(). | |
| 1248 | - | Int("stack_size", len(stackNode.Children)). | |
| 1249 | - | Int("insert_index", insertIndex). | |
| 1250 | - | Int("active_index", stackNode.ActiveStackIndex). | |
| 1251 | - | Msg("domain tree updated with position-aware insertion") | |
| 1282 | + | if stackCtx.activeNode.Parent != nil && stackCtx.activeNode.Parent.IsStacked { | |
| 1283 | + | // Already in a stack - use AddToStack use case | |
| 1284 | + | stackNode = stackCtx.activeNode.Parent | |
| 1285 | + | output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, newPane) | |
| 1286 | + | if err != nil { | |
| 1287 | + | log.Error().Err(err).Msg("failed to add pane to stack via use case") | |
| 1288 | + | return err | |
| 1289 | + | } | |
| 1290 | + | log.Debug(). | |
| 1291 | + | Int("stack_size", len(stackNode.Children)). | |
| 1292 | + | Int("insert_index", output.StackIndex). | |
| 1293 | + | Msg("added to existing stack via use case") | |
| 1294 | + | } else if stackCtx.activeNode.IsStacked { | |
| 1295 | + | // Active node is already a stack container - add to it | |
| 1296 | + | stackNode = stackCtx.activeNode | |
| 1297 | + | output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, newPane) | |
| 1298 | + | if err != nil { | |
| 1299 | + | log.Error().Err(err).Msg("failed to add pane to stack via use case") | |
| 1300 | + | return err | |
| 1301 | + | } | |
| 1302 | + | log.Debug(). | |
| 1303 | + | Int("stack_size", len(stackNode.Children)). | |
| 1304 | + | Int("insert_index", output.StackIndex). | |
| 1305 | + | Msg("added to stack container via use case") | |
| 1306 | + | } else { | |
| 1307 | + | // Need to create a new stack - use CreateStack use case | |
| 1308 | + | // But CreateStack creates its own pane, so we need a different approach | |
| 1309 | + | // Convert to stack first, then the new pane is already created | |
| 1310 | + | output, err := c.panesUC.CreateStack(ctx, stackCtx.ws, stackCtx.activeNode) | |
| 1311 | + | if err != nil { | |
| 1312 | + | log.Error().Err(err).Msg("failed to create stack via use case") | |
| 1313 | + | return err | |
| 1314 | + | } | |
| 1315 | + | stackNode = output.StackNode | |
| 1316 | + | // CreateStack creates its own new pane, use that instead | |
| 1317 | + | newPane = output.NewPane | |
| 1318 | + | newPaneID = newPane.ID | |
| 1319 | + | needsFirstPaneTitleUpdate = true | |
| 1320 | + | ||
| 1321 | + | // Update the original pane's title in the domain | |
| 1322 | + | if output.OriginalNode != nil && output.OriginalNode.Pane != nil { | |
| 1323 | + | output.OriginalNode.Pane.Title = stackCtx.originalTitle | |
| 1324 | + | } | |
| 1325 | + | ||
| 1326 | + | log.Debug(). | |
| 1327 | + | Int("stack_size", len(stackNode.Children)). | |
| 1328 | + | Msg("created new stack via use case") | |
| 1329 | + | } | |
| 1252 | 1330 | ||
| 1253 | 1331 | // Create PaneView for the new pane | |
| 1254 | 1332 | newPaneView := component.NewPaneView(c.widgetFactory, newPaneID, nil) | |
| @@ -1281,9 +1359,6 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error { | |||
| 1281 | 1359 | } | |
| 1282 | 1360 | } | |
| 1283 | 1361 | ||
| 1284 | - | // Update workspace active pane ID | |
| 1285 | - | stackCtx.ws.ActivePaneID = newPaneID | |
| 1286 | - | ||
| 1287 | 1362 | // Update workspace view | |
| 1288 | 1363 | if err := stackCtx.wsView.SetActivePaneID(newPaneID); err != nil { | |
| 1289 | 1364 | log.Warn().Err(err).Msg("failed to set active pane") | |
| @@ -1364,71 +1439,6 @@ func (c *WorkspaceCoordinator) prepareStackPane( | |||
| 1364 | 1439 | }, true | |
| 1365 | 1440 | } | |
| 1366 | 1441 | ||
| 1367 | - | func (c *WorkspaceCoordinator) ensureStackNode( | |
| 1368 | - | ctx context.Context, | |
| 1369 | - | activeNode *entity.PaneNode, | |
| 1370 | - | activePaneID entity.PaneID, | |
| 1371 | - | originalTitle string, | |
| 1372 | - | ) (*entity.PaneNode, bool) { | |
| 1373 | - | log := logging.FromContext(ctx) | |
| 1374 | - | if activeNode.Parent != nil && activeNode.Parent.IsStacked { | |
| 1375 | - | stackNode := activeNode.Parent | |
| 1376 | - | log.Debug(). | |
| 1377 | - | Str("stack_id", stackNode.ID). | |
| 1378 | - | Str("active_pane", string(activePaneID)). | |
| 1379 | - | Int("stack_size", len(stackNode.Children)). | |
| 1380 | - | Msg("adding to existing stack via parent") | |
| 1381 | - | return stackNode, false | |
| 1382 | - | } | |
| 1383 | - | ||
| 1384 | - | if activeNode.IsStacked { | |
| 1385 | - | return activeNode, false | |
| 1386 | - | } | |
| 1387 | - | ||
| 1388 | - | originalPane := activeNode.Pane | |
| 1389 | - | originalPane.Title = originalTitle | |
| 1390 | - | originalPaneChild := &entity.PaneNode{ | |
| 1391 | - | ID: activeNode.ID + "_0", | |
| 1392 | - | Pane: originalPane, | |
| 1393 | - | Parent: activeNode, | |
| 1394 | - | } | |
| 1395 | - | ||
| 1396 | - | activeNode.Pane = nil | |
| 1397 | - | activeNode.IsStacked = true | |
| 1398 | - | activeNode.Children = []*entity.PaneNode{originalPaneChild} | |
| 1399 | - | ||
| 1400 | - | log.Debug(). | |
| 1401 | - | Str("node_id", activeNode.ID). | |
| 1402 | - | Str("original_pane", string(originalPane.ID)). | |
| 1403 | - | Str("original_title", originalTitle). | |
| 1404 | - | Msg("converted leaf to stacked node") | |
| 1405 | - | ||
| 1406 | - | return activeNode, true | |
| 1407 | - | } | |
| 1408 | - | ||
| 1409 | - | func insertStackChild(stackNode *entity.PaneNode, newPaneID entity.PaneID, newPane *entity.Pane) int { | |
| 1410 | - | newChildNode := &entity.PaneNode{ | |
| 1411 | - | ID: stackNode.ID + "_" + string(newPaneID), | |
| 1412 | - | Pane: newPane, | |
| 1413 | - | Parent: stackNode, | |
| 1414 | - | } | |
| 1415 | - | ||
| 1416 | - | currentActiveIndex := stackNode.ActiveStackIndex | |
| 1417 | - | if currentActiveIndex < 0 || currentActiveIndex >= len(stackNode.Children) { | |
| 1418 | - | currentActiveIndex = len(stackNode.Children) - 1 | |
| 1419 | - | if currentActiveIndex < 0 { | |
| 1420 | - | currentActiveIndex = -1 | |
| 1421 | - | } | |
| 1422 | - | } | |
| 1423 | - | insertIndex := currentActiveIndex + 1 | |
| 1424 | - | ||
| 1425 | - | stackNode.Children = append(stackNode.Children, nil) | |
| 1426 | - | copy(stackNode.Children[insertIndex+1:], stackNode.Children[insertIndex:]) | |
| 1427 | - | stackNode.Children[insertIndex] = newChildNode | |
| 1428 | - | stackNode.ActiveStackIndex = insertIndex | |
| 1429 | - | return insertIndex | |
| 1430 | - | } | |
| 1431 | - | ||
| 1432 | 1442 | func (c *WorkspaceCoordinator) updateFirstStackTitle( | |
| 1433 | 1443 | ctx context.Context, | |
| 1434 | 1444 | wsView *component.WorkspaceView, | |
| @@ -1577,6 +1587,54 @@ func (c *WorkspaceCoordinator) onStackedPaneClose(ctx context.Context, paneID en | |||
| 1577 | 1587 | } | |
| 1578 | 1588 | } | |
| 1579 | 1589 | ||
| 1590 | + | // SetupStackedPaneCallbacks sets up title bar click and close callbacks for all stacked panes. | |
| 1591 | + | // This must be called after Rebuild() to restore callbacks on newly created StackedView instances. | |
| 1592 | + | func (c *WorkspaceCoordinator) SetupStackedPaneCallbacks(ctx context.Context, ws *entity.Workspace, wsView *component.WorkspaceView) { | |
| 1593 | + | log := logging.FromContext(ctx) | |
| 1594 | + | ||
| 1595 | + | if ws == nil || ws.Root == nil || wsView == nil { | |
| 1596 | + | return | |
| 1597 | + | } | |
| 1598 | + | ||
| 1599 | + | tr := wsView.TreeRenderer() | |
| 1600 | + | if tr == nil { | |
| 1601 | + | return | |
| 1602 | + | } | |
| 1603 | + | ||
| 1604 | + | // Walk the tree and set up callbacks for all stacked nodes | |
| 1605 | + | ws.Root.Walk(func(node *entity.PaneNode) bool { | |
| 1606 | + | if !node.IsStacked || len(node.Children) == 0 { | |
| 1607 | + | return true | |
| 1608 | + | } | |
| 1609 | + | ||
| 1610 | + | // Get the StackedView for any pane in this stack | |
| 1611 | + | firstChild := node.Children[0] | |
| 1612 | + | if firstChild.Pane == nil { | |
| 1613 | + | return true | |
| 1614 | + | } | |
| 1615 | + | ||
| 1616 | + | stackedView := tr.GetStackedViewForPane(string(firstChild.Pane.ID)) | |
| 1617 | + | if stackedView == nil { | |
| 1618 | + | return true | |
| 1619 | + | } | |
| 1620 | + | ||
| 1621 | + | capturedStackNode := node | |
| 1622 | + | stackedView.SetOnActivate(func(index int) { | |
| 1623 | + | c.onTitleBarClick(ctx, capturedStackNode, stackedView, index) | |
| 1624 | + | }) | |
| 1625 | + | stackedView.SetOnClosePane(func(paneID string) { | |
| 1626 | + | c.onStackedPaneClose(ctx, entity.PaneID(paneID)) | |
| 1627 | + | }) | |
| 1628 | + | ||
| 1629 | + | log.Debug(). | |
| 1630 | + | Str("stack_id", node.ID). | |
| 1631 | + | Int("stack_size", len(node.Children)). | |
| 1632 | + | Msg("set up stacked pane callbacks") | |
| 1633 | + | ||
| 1634 | + | return true | |
| 1635 | + | }) | |
| 1636 | + | } | |
| 1637 | + | ||
| 1580 | 1638 | // --- Popup Insertion --- | |
| 1581 | 1639 | ||
| 1582 | 1640 | // SetOnCreatePopupTab sets the callback for creating popup tabs. | |
| @@ -1726,6 +1784,7 @@ func (c *WorkspaceCoordinator) attachPopupWebView( | |||
| 1726 | 1784 | } | |
| 1727 | 1785 | ||
| 1728 | 1786 | // insertPopupStacked inserts a popup as a stacked pane on top of the parent. | |
| 1787 | + | // Uses CreateStack and AddToStack use cases for proper domain model management. | |
| 1729 | 1788 | func (c *WorkspaceCoordinator) insertPopupStacked(ctx context.Context, input InsertPopupInput) error { | |
| 1730 | 1789 | log := logging.FromContext(ctx) | |
| 1731 | 1790 | ||
| @@ -1739,16 +1798,17 @@ func (c *WorkspaceCoordinator) insertPopupStacked(ctx context.Context, input Ins | |||
| 1739 | 1798 | return fmt.Errorf("parent pane not found: %s", input.ParentPaneID) | |
| 1740 | 1799 | } | |
| 1741 | 1800 | ||
| 1742 | - | stackNode := c.resolveStackNode(input, parentNode, log) | |
| 1743 | - | if stackNode == nil { | |
| 1744 | - | return fmt.Errorf("failed to resolve stack node for popup") | |
| 1745 | - | } | |
| 1801 | + | // Resolve or create stack node | |
| 1802 | + | stackNode := c.resolveOrCreateStackNode(ctx, parentNode, input.ParentPaneID) | |
| 1746 | 1803 | ||
| 1747 | - | c.insertStackChild(stackNode, input.PopupPane) | |
| 1804 | + | // Add popup pane to stack using use case | |
| 1805 | + | if _, err := c.panesUC.AddToStack(ctx, ws, stackNode, input.PopupPane); err != nil { | |
| 1806 | + | return fmt.Errorf("failed to add popup to stack: %w", err) | |
| 1807 | + | } | |
| 1748 | 1808 | ||
| 1749 | 1809 | if wsView == nil { | |
| 1750 | - | // No workspace view to update; keep domain model consistent. | |
| 1751 | - | ws.ActivePaneID = input.PopupPane.ID | |
| 1810 | + | // No workspace view to update; domain model already updated by use case. | |
| 1811 | + | return nil | |
| 1752 | 1812 | } | |
| 1753 | 1813 | ||
| 1754 | 1814 | if err := c.attachPopupPaneView(ctx, input, wsView, stackNode, log); err != nil { | |
| @@ -1763,27 +1823,37 @@ func (c *WorkspaceCoordinator) insertPopupStacked(ctx context.Context, input Ins | |||
| 1763 | 1823 | return nil | |
| 1764 | 1824 | } | |
| 1765 | 1825 | ||
| 1766 | - | func (c *WorkspaceCoordinator) resolveStackNode( | |
| 1767 | - | input InsertPopupInput, | |
| 1826 | + | // resolveOrCreateStackNode finds an existing stack or creates one for popup insertion. | |
| 1827 | + | // Note: For popups, we can't use CreateStack use case since it creates a new pane, | |
| 1828 | + | // but we already have the popup pane. This conversion is a special case. | |
| 1829 | + | func (c *WorkspaceCoordinator) resolveOrCreateStackNode( | |
| 1830 | + | ctx context.Context, | |
| 1768 | 1831 | parentNode *entity.PaneNode, | |
| 1769 | - | log *zerolog.Logger, | |
| 1832 | + | parentPaneID entity.PaneID, | |
| 1770 | 1833 | ) *entity.PaneNode { | |
| 1834 | + | log := logging.FromContext(ctx) | |
| 1835 | + | ||
| 1836 | + | // Check if parent is already in a stack | |
| 1771 | 1837 | if parentNode.Parent != nil && parentNode.Parent.IsStacked { | |
| 1838 | + | log.Debug(). | |
| 1839 | + | Str("stack_id", parentNode.Parent.ID). | |
| 1840 | + | Msg("using existing parent stack for popup") | |
| 1772 | 1841 | return parentNode.Parent | |
| 1773 | 1842 | } | |
| 1843 | + | ||
| 1844 | + | // Check if parent is already a stack container | |
| 1774 | 1845 | if parentNode.IsStacked { | |
| 1846 | + | log.Debug(). | |
| 1847 | + | Str("stack_id", parentNode.ID). | |
| 1848 | + | Msg("parent is already a stack container") | |
| 1775 | 1849 | return parentNode | |
| 1776 | 1850 | } | |
| 1777 | - | return c.convertToStackedNode(input, parentNode, log) | |
| 1778 | - | } | |
| 1779 | 1851 | ||
| 1780 | - | func (c *WorkspaceCoordinator) convertToStackedNode( | |
| 1781 | - | input InsertPopupInput, | |
| 1782 | - | parentNode *entity.PaneNode, | |
| 1783 | - | log *zerolog.Logger, | |
| 1784 | - | ) *entity.PaneNode { | |
| 1852 | + | // Need to convert leaf to stack for popup insertion. | |
| 1853 | + | // We can't use CreateStack use case since it creates a new pane, | |
| 1854 | + | // but we already have the popup pane to insert. | |
| 1785 | 1855 | originalPane := parentNode.Pane | |
| 1786 | - | originalTitle := c.contentCoord.GetTitle(input.ParentPaneID) | |
| 1856 | + | originalTitle := c.contentCoord.GetTitle(parentPaneID) | |
| 1787 | 1857 | if originalTitle == "" { | |
| 1788 | 1858 | originalTitle = originalPane.Title | |
| 1789 | 1859 | } | |
| @@ -1792,14 +1862,17 @@ func (c *WorkspaceCoordinator) convertToStackedNode( | |||
| 1792 | 1862 | } | |
| 1793 | 1863 | originalPane.Title = originalTitle | |
| 1794 | 1864 | ||
| 1865 | + | // Create child node for the original pane | |
| 1795 | 1866 | originalPaneChild := &entity.PaneNode{ | |
| 1796 | 1867 | ID: parentNode.ID + "_0", | |
| 1797 | 1868 | Pane: originalPane, | |
| 1798 | 1869 | Parent: parentNode, | |
| 1799 | 1870 | } | |
| 1800 | 1871 | ||
| 1872 | + | // Convert parentNode to stack container | |
| 1801 | 1873 | parentNode.Pane = nil | |
| 1802 | 1874 | parentNode.IsStacked = true | |
| 1875 | + | parentNode.ActiveStackIndex = 0 | |
| 1803 | 1876 | parentNode.Children = []*entity.PaneNode{originalPaneChild} | |
| 1804 | 1877 | ||
| 1805 | 1878 | log.Debug(). | |
| @@ -1810,28 +1883,6 @@ func (c *WorkspaceCoordinator) convertToStackedNode( | |||
| 1810 | 1883 | return parentNode | |
| 1811 | 1884 | } | |
| 1812 | 1885 | ||
| 1813 | - | func (c *WorkspaceCoordinator) insertStackChild(stackNode *entity.PaneNode, pane *entity.Pane) { | |
| 1814 | - | newChildNode := &entity.PaneNode{ | |
| 1815 | - | ID: stackNode.ID + "_popup_" + string(pane.ID), | |
| 1816 | - | Pane: pane, | |
| 1817 | - | Parent: stackNode, | |
| 1818 | - | } | |
| 1819 | - | ||
| 1820 | - | currentActiveIndex := stackNode.ActiveStackIndex | |
| 1821 | - | if currentActiveIndex < 0 || currentActiveIndex >= len(stackNode.Children) { | |
| 1822 | - | currentActiveIndex = len(stackNode.Children) - 1 | |
| 1823 | - | if currentActiveIndex < 0 { | |
| 1824 | - | currentActiveIndex = -1 | |
| 1825 | - | } | |
| 1826 | - | } | |
| 1827 | - | insertIndex := currentActiveIndex + 1 | |
| 1828 | - | ||
| 1829 | - | stackNode.Children = append(stackNode.Children, nil) | |
| 1830 | - | copy(stackNode.Children[insertIndex+1:], stackNode.Children[insertIndex:]) | |
| 1831 | - | stackNode.Children[insertIndex] = newChildNode | |
| 1832 | - | stackNode.ActiveStackIndex = insertIndex | |
| 1833 | - | } | |
| 1834 | - | ||
| 1835 | 1886 | func (c *WorkspaceCoordinator) attachPopupPaneView( | |
| 1836 | 1887 | ctx context.Context, | |
| 1837 | 1888 | input InsertPopupInput, | |
| @@ -1933,6 +1984,7 @@ func (c *WorkspaceCoordinator) ConsumeOrExpelPane(ctx context.Context, direction | |||
| 1933 | 1984 | log.Warn().Err(err).Msg("failed to rebuild workspace view") | |
| 1934 | 1985 | } | |
| 1935 | 1986 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | |
| 1987 | + | c.SetupStackedPaneCallbacks(ctx, ws, wsView) | |
| 1936 | 1988 | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | |
| 1937 | 1989 | log.Warn().Err(err).Msg("failed to set active pane in workspace view") | |
| 1938 | 1990 | } | |
| @@ -263,6 +263,7 @@ func (c *WorkspaceCoordinator) applySplitToView( | |||||
| 263 | 263 | ||||
| 264 | if needsAttach { | 264 | if needsAttach { | ||
| 265 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | 265 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | ||
| 266 | + | c.SetupStackedPaneCallbacks(ctx, ws, wsView) | |||
| 266 | } | 267 | } | ||
| 267 | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | 268 | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | ||
| 268 | log.Warn().Err(err).Msg("failed to set active pane in workspace view") | 269 | log.Warn().Err(err).Msg("failed to set active pane in workspace view") | ||
| @@ -1055,6 +1056,7 @@ func (c *WorkspaceCoordinator) finalizePaneClose( | |||||
| 1055 | } | 1056 | } | ||
| 1056 | c.contentCoord.ReleaseWebView(ctx, paneID) | 1057 | c.contentCoord.ReleaseWebView(ctx, paneID) | ||
| 1057 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | 1058 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | ||
| 1059 | + | c.SetupStackedPaneCallbacks(ctx, ws, wsView) | |||
| 1058 | } | 1060 | } | ||
| 1059 | } else { | 1061 | } else { | ||
| 1060 | if err := wsView.Rebuild(ctx); err != nil { | 1062 | if err := wsView.Rebuild(ctx); err != nil { | ||
| @@ -1062,6 +1064,7 @@ func (c *WorkspaceCoordinator) finalizePaneClose( | |||||
| 1062 | } | 1064 | } | ||
| 1063 | c.contentCoord.ReleaseWebView(ctx, paneID) | 1065 | c.contentCoord.ReleaseWebView(ctx, paneID) | ||
| 1064 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | 1066 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | ||
| 1067 | + | c.SetupStackedPaneCallbacks(ctx, ws, wsView) | |||
| 1065 | } | 1068 | } | ||
| 1066 | 1069 | ||||
| 1067 | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | 1070 | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | ||
| @@ -1095,17 +1098,10 @@ func (c *WorkspaceCoordinator) doIncrementalStackClose( | |||||
| 1095 | return fmt.Errorf("stacked view not found for pane %s", closingPaneID) | 1098 | return fmt.Errorf("stacked view not found for pane %s", closingPaneID) | ||
| 1096 | } | 1099 | } | ||
| 1097 | 1100 | ||||
| 1098 | - | // Find the index of the closing pane in the stack | 1101 | + | // Find the index of the closing pane in the StackedView UI |
| 1099 | - | closingIndex := -1 | 1102 | + | closingIndex := stackedView.FindPaneIndex(string(closingPaneID)) |
| 1100 | - | for i, child := range stackNode.Children { | |||
| 1101 | - | if child.Pane != nil && child.Pane.ID == closingPaneID { | |||
| 1102 | - | closingIndex = i | |||
| 1103 | - | break | |||
| 1104 | - | } | |||
| 1105 | - | } | |||
| 1106 | - | ||||
| 1107 | if closingIndex < 0 { | 1103 | if closingIndex < 0 { | ||
| 1108 | - | return fmt.Errorf("pane %s not found in stack", closingPaneID) | 1104 | + | return fmt.Errorf("pane %s not found in stacked view", closingPaneID) |
| 1109 | } | 1105 | } | ||
| 1110 | 1106 | ||||
| 1111 | // Remove the pane from the StackedView widget | 1107 | // Remove the pane from the StackedView widget | ||
| @@ -1120,12 +1116,48 @@ func (c *WorkspaceCoordinator) doIncrementalStackClose( | |||||
| 1120 | // Release the closing pane's webview | 1116 | // Release the closing pane's webview | ||
| 1121 | c.contentCoord.ReleaseWebView(ctx, closingPaneID) | 1117 | c.contentCoord.ReleaseWebView(ctx, closingPaneID) | ||
| 1122 | 1118 | ||||
| 1119 | + | // Sync remaining title bars with current titles from ContentCoordinator. | |||
| 1120 | + | // The domain model (stackNode.Children) has already been updated by the use case, | |||
| 1121 | + | // so we iterate the remaining children and update their titles in the UI. | |||
| 1122 | + | c.syncStackedPaneTitles(ctx, stackedView, stackNode) | |||
| 1123 | + | ||||
| 1123 | log.Debug(). | 1124 | log.Debug(). | ||
| 1124 | Int("removed_index", closingIndex). | 1125 | Int("removed_index", closingIndex). | ||
| 1125 | Msg("incremental stack close completed successfully") | 1126 | Msg("incremental stack close completed successfully") | ||
| 1126 | return nil | 1127 | return nil | ||
| 1127 | } | 1128 | } | ||
| 1128 | 1129 | ||||
| 1130 | + | // syncStackedPaneTitles updates all title bars in a StackedView with current titles. | |||
| 1131 | + | func (c *WorkspaceCoordinator) syncStackedPaneTitles(ctx context.Context, stackedView *layout.StackedView, stackNode *entity.PaneNode) { | |||
| 1132 | + | log := logging.FromContext(ctx) | |||
| 1133 | + | ||||
| 1134 | + | if stackedView == nil || stackNode == nil { | |||
| 1135 | + | return | |||
| 1136 | + | } | |||
| 1137 | + | ||||
| 1138 | + | for i, child := range stackNode.Children { | |||
| 1139 | + | if child.Pane == nil { | |||
| 1140 | + | continue | |||
| 1141 | + | } | |||
| 1142 | + | ||||
| 1143 | + | title := c.contentCoord.GetTitle(child.Pane.ID) | |||
| 1144 | + | if title == "" { | |||
| 1145 | + | title = child.Pane.Title | |||
| 1146 | + | } | |||
| 1147 | + | if title == "" { | |||
| 1148 | + | title = defaultPaneTitle | |||
| 1149 | + | } | |||
| 1150 | + | ||||
| 1151 | + | if err := stackedView.UpdateTitle(i, title); err != nil { | |||
| 1152 | + | log.Warn(). | |||
| 1153 | + | Err(err). | |||
| 1154 | + | Int("index", i). | |||
| 1155 | + | Str("pane_id", string(child.Pane.ID)). | |||
| 1156 | + | Msg("failed to sync title bar") | |||
| 1157 | + | } | |||
| 1158 | + | } | |||
| 1159 | + | } | |||
| 1160 | + | ||||
| 1129 | // FocusPane navigates focus to an adjacent pane. | 1161 | // FocusPane navigates focus to an adjacent pane. | ||
| 1130 | func (c *WorkspaceCoordinator) FocusPane(ctx context.Context, direction usecase.NavigateDirection) error { | 1162 | func (c *WorkspaceCoordinator) FocusPane(ctx context.Context, direction usecase.NavigateDirection) error { | ||
| 1131 | log := logging.FromContext(ctx) | 1163 | log := logging.FromContext(ctx) | ||
| @@ -1228,6 +1260,7 @@ func (c *WorkspaceCoordinator) syncStackedViewActive(ctx context.Context, wsView | |||||
| 1228 | } | 1260 | } | ||
| 1229 | 1261 | ||||
| 1230 | // StackPane adds a new pane stacked on top of the active pane. | 1262 | // StackPane adds a new pane stacked on top of the active pane. | ||
| 1263 | + | // Uses CreateStack use case for new stacks, AddToStack for existing stacks. | |||
| 1231 | func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error { | 1264 | func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error { | ||
| 1232 | log := logging.FromContext(ctx) | 1265 | log := logging.FromContext(ctx) | ||
| 1233 | 1266 | ||||
| @@ -1236,19 +1269,64 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error { | |||||
| 1236 | return nil | 1269 | return nil | ||
| 1237 | } | 1270 | } | ||
| 1238 | 1271 | ||||
| 1272 | + | // Create new pane entity | |||
| 1239 | newPaneID := entity.PaneID(c.generateID()) | 1273 | newPaneID := entity.PaneID(c.generateID()) | ||
| 1240 | newPane := entity.NewPane(newPaneID) | 1274 | newPane := entity.NewPane(newPaneID) | ||
| 1241 | newPane.URI = domainurl.Normalize(config.Get().Workspace.NewPaneURL) | 1275 | newPane.URI = domainurl.Normalize(config.Get().Workspace.NewPaneURL) | ||
| 1242 | newPane.Title = defaultPaneTitle | 1276 | newPane.Title = defaultPaneTitle | ||
| 1243 | 1277 | ||||
| 1244 | - | stackNode, needsFirstPaneTitleUpdate := c.ensureStackNode(ctx, stackCtx.activeNode, stackCtx.activePaneID, stackCtx.originalTitle) | 1278 | + | // Determine if we need to create a new stack or add to existing |
| 1245 | - | insertIndex := insertStackChild(stackNode, newPaneID, newPane) | 1279 | + | var stackNode *entity.PaneNode |
| 1280 | + | var needsFirstPaneTitleUpdate bool | |||
| 1246 | 1281 | ||||
| 1247 | - | log.Debug(). | 1282 | + | if stackCtx.activeNode.Parent != nil && stackCtx.activeNode.Parent.IsStacked { |
| 1248 | - | Int("stack_size", len(stackNode.Children)). | 1283 | + | // Already in a stack - use AddToStack use case |
| 1249 | - | Int("insert_index", insertIndex). | 1284 | + | stackNode = stackCtx.activeNode.Parent |
| 1250 | - | Int("active_index", stackNode.ActiveStackIndex). | 1285 | + | output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, newPane) |
| 1251 | - | Msg("domain tree updated with position-aware insertion") | 1286 | + | if err != nil { |
| 1287 | + | log.Error().Err(err).Msg("failed to add pane to stack via use case") | |||
| 1288 | + | return err | |||
| 1289 | + | } | |||
| 1290 | + | log.Debug(). | |||
| 1291 | + | Int("stack_size", len(stackNode.Children)). | |||
| 1292 | + | Int("insert_index", output.StackIndex). | |||
| 1293 | + | Msg("added to existing stack via use case") | |||
| 1294 | + | } else if stackCtx.activeNode.IsStacked { | |||
| 1295 | + | // Active node is already a stack container - add to it | |||
| 1296 | + | stackNode = stackCtx.activeNode | |||
| 1297 | + | output, err := c.panesUC.AddToStack(ctx, stackCtx.ws, stackNode, newPane) | |||
| 1298 | + | if err != nil { | |||
| 1299 | + | log.Error().Err(err).Msg("failed to add pane to stack via use case") | |||
| 1300 | + | return err | |||
| 1301 | + | } | |||
| 1302 | + | log.Debug(). | |||
| 1303 | + | Int("stack_size", len(stackNode.Children)). | |||
| 1304 | + | Int("insert_index", output.StackIndex). | |||
| 1305 | + | Msg("added to stack container via use case") | |||
| 1306 | + | } else { | |||
| 1307 | + | // Need to create a new stack - use CreateStack use case | |||
| 1308 | + | // But CreateStack creates its own pane, so we need a different approach | |||
| 1309 | + | // Convert to stack first, then the new pane is already created | |||
| 1310 | + | output, err := c.panesUC.CreateStack(ctx, stackCtx.ws, stackCtx.activeNode) | |||
| 1311 | + | if err != nil { | |||
| 1312 | + | log.Error().Err(err).Msg("failed to create stack via use case") | |||
| 1313 | + | return err | |||
| 1314 | + | } | |||
| 1315 | + | stackNode = output.StackNode | |||
| 1316 | + | // CreateStack creates its own new pane, use that instead | |||
| 1317 | + | newPane = output.NewPane | |||
| 1318 | + | newPaneID = newPane.ID | |||
| 1319 | + | needsFirstPaneTitleUpdate = true | |||
| 1320 | + | ||||
| 1321 | + | // Update the original pane's title in the domain | |||
| 1322 | + | if output.OriginalNode != nil && output.OriginalNode.Pane != nil { | |||
| 1323 | + | output.OriginalNode.Pane.Title = stackCtx.originalTitle | |||
| 1324 | + | } | |||
| 1325 | + | ||||
| 1326 | + | log.Debug(). | |||
| 1327 | + | Int("stack_size", len(stackNode.Children)). | |||
| 1328 | + | Msg("created new stack via use case") | |||
| 1329 | + | } | |||
| 1252 | 1330 | ||||
| 1253 | // Create PaneView for the new pane | 1331 | // Create PaneView for the new pane | ||
| 1254 | newPaneView := component.NewPaneView(c.widgetFactory, newPaneID, nil) | 1332 | newPaneView := component.NewPaneView(c.widgetFactory, newPaneID, nil) | ||
| @@ -1281,9 +1359,6 @@ func (c *WorkspaceCoordinator) StackPane(ctx context.Context) error { | |||||
| 1281 | } | 1359 | } | ||
| 1282 | } | 1360 | } | ||
| 1283 | 1361 | ||||
| 1284 | - | // Update workspace active pane ID | |||
| 1285 | - | stackCtx.ws.ActivePaneID = newPaneID | |||
| 1286 | - | ||||
| 1287 | // Update workspace view | 1362 | // Update workspace view | ||
| 1288 | if err := stackCtx.wsView.SetActivePaneID(newPaneID); err != nil { | 1363 | if err := stackCtx.wsView.SetActivePaneID(newPaneID); err != nil { | ||
| 1289 | log.Warn().Err(err).Msg("failed to set active pane") | 1364 | log.Warn().Err(err).Msg("failed to set active pane") | ||
| @@ -1364,71 +1439,6 @@ func (c *WorkspaceCoordinator) prepareStackPane( | |||||
| 1364 | }, true | 1439 | }, true | ||
| 1365 | } | 1440 | } | ||
| 1366 | 1441 | ||||
| 1367 | - | func (c *WorkspaceCoordinator) ensureStackNode( | |||
| 1368 | - | ctx context.Context, | |||
| 1369 | - | activeNode *entity.PaneNode, | |||
| 1370 | - | activePaneID entity.PaneID, | |||
| 1371 | - | originalTitle string, | |||
| 1372 | - | ) (*entity.PaneNode, bool) { | |||
| 1373 | - | log := logging.FromContext(ctx) | |||
| 1374 | - | if activeNode.Parent != nil && activeNode.Parent.IsStacked { | |||
| 1375 | - | stackNode := activeNode.Parent | |||
| 1376 | - | log.Debug(). | |||
| 1377 | - | Str("stack_id", stackNode.ID). | |||
| 1378 | - | Str("active_pane", string(activePaneID)). | |||
| 1379 | - | Int("stack_size", len(stackNode.Children)). | |||
| 1380 | - | Msg("adding to existing stack via parent") | |||
| 1381 | - | return stackNode, false | |||
| 1382 | - | } | |||
| 1383 | - | ||||
| 1384 | - | if activeNode.IsStacked { | |||
| 1385 | - | return activeNode, false | |||
| 1386 | - | } | |||
| 1387 | - | ||||
| 1388 | - | originalPane := activeNode.Pane | |||
| 1389 | - | originalPane.Title = originalTitle | |||
| 1390 | - | originalPaneChild := &entity.PaneNode{ | |||
| 1391 | - | ID: activeNode.ID + "_0", | |||
| 1392 | - | Pane: originalPane, | |||
| 1393 | - | Parent: activeNode, | |||
| 1394 | - | } | |||
| 1395 | - | ||||
| 1396 | - | activeNode.Pane = nil | |||
| 1397 | - | activeNode.IsStacked = true | |||
| 1398 | - | activeNode.Children = []*entity.PaneNode{originalPaneChild} | |||
| 1399 | - | ||||
| 1400 | - | log.Debug(). | |||
| 1401 | - | Str("node_id", activeNode.ID). | |||
| 1402 | - | Str("original_pane", string(originalPane.ID)). | |||
| 1403 | - | Str("original_title", originalTitle). | |||
| 1404 | - | Msg("converted leaf to stacked node") | |||
| 1405 | - | ||||
| 1406 | - | return activeNode, true | |||
| 1407 | - | } | |||
| 1408 | - | ||||
| 1409 | - | func insertStackChild(stackNode *entity.PaneNode, newPaneID entity.PaneID, newPane *entity.Pane) int { | |||
| 1410 | - | newChildNode := &entity.PaneNode{ | |||
| 1411 | - | ID: stackNode.ID + "_" + string(newPaneID), | |||
| 1412 | - | Pane: newPane, | |||
| 1413 | - | Parent: stackNode, | |||
| 1414 | - | } | |||
| 1415 | - | ||||
| 1416 | - | currentActiveIndex := stackNode.ActiveStackIndex | |||
| 1417 | - | if currentActiveIndex < 0 || currentActiveIndex >= len(stackNode.Children) { | |||
| 1418 | - | currentActiveIndex = len(stackNode.Children) - 1 | |||
| 1419 | - | if currentActiveIndex < 0 { | |||
| 1420 | - | currentActiveIndex = -1 | |||
| 1421 | - | } | |||
| 1422 | - | } | |||
| 1423 | - | insertIndex := currentActiveIndex + 1 | |||
| 1424 | - | ||||
| 1425 | - | stackNode.Children = append(stackNode.Children, nil) | |||
| 1426 | - | copy(stackNode.Children[insertIndex+1:], stackNode.Children[insertIndex:]) | |||
| 1427 | - | stackNode.Children[insertIndex] = newChildNode | |||
| 1428 | - | stackNode.ActiveStackIndex = insertIndex | |||
| 1429 | - | return insertIndex | |||
| 1430 | - | } | |||
| 1431 | - | ||||
| 1432 | func (c *WorkspaceCoordinator) updateFirstStackTitle( | 1442 | func (c *WorkspaceCoordinator) updateFirstStackTitle( | ||
| 1433 | ctx context.Context, | 1443 | ctx context.Context, | ||
| 1434 | wsView *component.WorkspaceView, | 1444 | wsView *component.WorkspaceView, | ||
| @@ -1577,6 +1587,54 @@ func (c *WorkspaceCoordinator) onStackedPaneClose(ctx context.Context, paneID en | |||||
| 1577 | } | 1587 | } | ||
| 1578 | } | 1588 | } | ||
| 1579 | 1589 | ||||
| 1590 | + | // SetupStackedPaneCallbacks sets up title bar click and close callbacks for all stacked panes. | |||
| 1591 | + | // This must be called after Rebuild() to restore callbacks on newly created StackedView instances. | |||
| 1592 | + | func (c *WorkspaceCoordinator) SetupStackedPaneCallbacks(ctx context.Context, ws *entity.Workspace, wsView *component.WorkspaceView) { | |||
| 1593 | + | log := logging.FromContext(ctx) | |||
| 1594 | + | ||||
| 1595 | + | if ws == nil || ws.Root == nil || wsView == nil { | |||
| 1596 | + | return | |||
| 1597 | + | } | |||
| 1598 | + | ||||
| 1599 | + | tr := wsView.TreeRenderer() | |||
| 1600 | + | if tr == nil { | |||
| 1601 | + | return | |||
| 1602 | + | } | |||
| 1603 | + | ||||
| 1604 | + | // Walk the tree and set up callbacks for all stacked nodes | |||
| 1605 | + | ws.Root.Walk(func(node *entity.PaneNode) bool { | |||
| 1606 | + | if !node.IsStacked || len(node.Children) == 0 { | |||
| 1607 | + | return true | |||
| 1608 | + | } | |||
| 1609 | + | ||||
| 1610 | + | // Get the StackedView for any pane in this stack | |||
| 1611 | + | firstChild := node.Children[0] | |||
| 1612 | + | if firstChild.Pane == nil { | |||
| 1613 | + | return true | |||
| 1614 | + | } | |||
| 1615 | + | ||||
| 1616 | + | stackedView := tr.GetStackedViewForPane(string(firstChild.Pane.ID)) | |||
| 1617 | + | if stackedView == nil { | |||
| 1618 | + | return true | |||
| 1619 | + | } | |||
| 1620 | + | ||||
| 1621 | + | capturedStackNode := node | |||
| 1622 | + | stackedView.SetOnActivate(func(index int) { | |||
| 1623 | + | c.onTitleBarClick(ctx, capturedStackNode, stackedView, index) | |||
| 1624 | + | }) | |||
| 1625 | + | stackedView.SetOnClosePane(func(paneID string) { | |||
| 1626 | + | c.onStackedPaneClose(ctx, entity.PaneID(paneID)) | |||
| 1627 | + | }) | |||
| 1628 | + | ||||
| 1629 | + | log.Debug(). | |||
| 1630 | + | Str("stack_id", node.ID). | |||
| 1631 | + | Int("stack_size", len(node.Children)). | |||
| 1632 | + | Msg("set up stacked pane callbacks") | |||
| 1633 | + | ||||
| 1634 | + | return true | |||
| 1635 | + | }) | |||
| 1636 | + | } | |||
| 1637 | + | ||||
| 1580 | // --- Popup Insertion --- | 1638 | // --- Popup Insertion --- | ||
| 1581 | 1639 | ||||
| 1582 | // SetOnCreatePopupTab sets the callback for creating popup tabs. | 1640 | // SetOnCreatePopupTab sets the callback for creating popup tabs. | ||
| @@ -1726,6 +1784,7 @@ func (c *WorkspaceCoordinator) attachPopupWebView( | |||||
| 1726 | } | 1784 | } | ||
| 1727 | 1785 | ||||
| 1728 | // insertPopupStacked inserts a popup as a stacked pane on top of the parent. | 1786 | // insertPopupStacked inserts a popup as a stacked pane on top of the parent. | ||
| 1787 | + | // Uses CreateStack and AddToStack use cases for proper domain model management. | |||
| 1729 | func (c *WorkspaceCoordinator) insertPopupStacked(ctx context.Context, input InsertPopupInput) error { | 1788 | func (c *WorkspaceCoordinator) insertPopupStacked(ctx context.Context, input InsertPopupInput) error { | ||
| 1730 | log := logging.FromContext(ctx) | 1789 | log := logging.FromContext(ctx) | ||
| 1731 | 1790 | ||||
| @@ -1739,16 +1798,17 @@ func (c *WorkspaceCoordinator) insertPopupStacked(ctx context.Context, input Ins | |||||
| 1739 | return fmt.Errorf("parent pane not found: %s", input.ParentPaneID) | 1798 | return fmt.Errorf("parent pane not found: %s", input.ParentPaneID) | ||
| 1740 | } | 1799 | } | ||
| 1741 | 1800 | ||||
| 1742 | - | stackNode := c.resolveStackNode(input, parentNode, log) | 1801 | + | // Resolve or create stack node |
| 1743 | - | if stackNode == nil { | 1802 | + | stackNode := c.resolveOrCreateStackNode(ctx, parentNode, input.ParentPaneID) |
| 1744 | - | return fmt.Errorf("failed to resolve stack node for popup") | |||
| 1745 | - | } | |||
| 1746 | 1803 | ||||
| 1747 | - | c.insertStackChild(stackNode, input.PopupPane) | 1804 | + | // Add popup pane to stack using use case |
| 1805 | + | if _, err := c.panesUC.AddToStack(ctx, ws, stackNode, input.PopupPane); err != nil { | |||
| 1806 | + | return fmt.Errorf("failed to add popup to stack: %w", err) | |||
| 1807 | + | } | |||
| 1748 | 1808 | ||||
| 1749 | if wsView == nil { | 1809 | if wsView == nil { | ||
| 1750 | - | // No workspace view to update; keep domain model consistent. | 1810 | + | // No workspace view to update; domain model already updated by use case. |
| 1751 | - | ws.ActivePaneID = input.PopupPane.ID | 1811 | + | return nil |
| 1752 | } | 1812 | } | ||
| 1753 | 1813 | ||||
| 1754 | if err := c.attachPopupPaneView(ctx, input, wsView, stackNode, log); err != nil { | 1814 | if err := c.attachPopupPaneView(ctx, input, wsView, stackNode, log); err != nil { | ||
| @@ -1763,27 +1823,37 @@ func (c *WorkspaceCoordinator) insertPopupStacked(ctx context.Context, input Ins | |||||
| 1763 | return nil | 1823 | return nil | ||
| 1764 | } | 1824 | } | ||
| 1765 | 1825 | ||||
| 1766 | - | func (c *WorkspaceCoordinator) resolveStackNode( | 1826 | + | // resolveOrCreateStackNode finds an existing stack or creates one for popup insertion. |
| 1767 | - | input InsertPopupInput, | 1827 | + | // Note: For popups, we can't use CreateStack use case since it creates a new pane, |
| 1828 | + | // but we already have the popup pane. This conversion is a special case. | |||
| 1829 | + | func (c *WorkspaceCoordinator) resolveOrCreateStackNode( | |||
| 1830 | + | ctx context.Context, | |||
| 1768 | parentNode *entity.PaneNode, | 1831 | parentNode *entity.PaneNode, | ||
| 1769 | - | log *zerolog.Logger, | 1832 | + | parentPaneID entity.PaneID, |
| 1770 | ) *entity.PaneNode { | 1833 | ) *entity.PaneNode { | ||
| 1834 | + | log := logging.FromContext(ctx) | |||
| 1835 | + | ||||
| 1836 | + | // Check if parent is already in a stack | |||
| 1771 | if parentNode.Parent != nil && parentNode.Parent.IsStacked { | 1837 | if parentNode.Parent != nil && parentNode.Parent.IsStacked { | ||
| 1838 | + | log.Debug(). | |||
| 1839 | + | Str("stack_id", parentNode.Parent.ID). | |||
| 1840 | + | Msg("using existing parent stack for popup") | |||
| 1772 | return parentNode.Parent | 1841 | return parentNode.Parent | ||
| 1773 | } | 1842 | } | ||
| 1843 | + | ||||
| 1844 | + | // Check if parent is already a stack container | |||
| 1774 | if parentNode.IsStacked { | 1845 | if parentNode.IsStacked { | ||
| 1846 | + | log.Debug(). | |||
| 1847 | + | Str("stack_id", parentNode.ID). | |||
| 1848 | + | Msg("parent is already a stack container") | |||
| 1775 | return parentNode | 1849 | return parentNode | ||
| 1776 | } | 1850 | } | ||
| 1777 | - | return c.convertToStackedNode(input, parentNode, log) | |||
| 1778 | - | } | |||
| 1779 | 1851 | ||||
| 1780 | - | func (c *WorkspaceCoordinator) convertToStackedNode( | 1852 | + | // Need to convert leaf to stack for popup insertion. |
| 1781 | - | input InsertPopupInput, | 1853 | + | // We can't use CreateStack use case since it creates a new pane, |
| 1782 | - | parentNode *entity.PaneNode, | 1854 | + | // but we already have the popup pane to insert. |
| 1783 | - | log *zerolog.Logger, | |||
| 1784 | - | ) *entity.PaneNode { | |||
| 1785 | originalPane := parentNode.Pane | 1855 | originalPane := parentNode.Pane | ||
| 1786 | - | originalTitle := c.contentCoord.GetTitle(input.ParentPaneID) | 1856 | + | originalTitle := c.contentCoord.GetTitle(parentPaneID) |
| 1787 | if originalTitle == "" { | 1857 | if originalTitle == "" { | ||
| 1788 | originalTitle = originalPane.Title | 1858 | originalTitle = originalPane.Title | ||
| 1789 | } | 1859 | } | ||
| @@ -1792,14 +1862,17 @@ func (c *WorkspaceCoordinator) convertToStackedNode( | |||||
| 1792 | } | 1862 | } | ||
| 1793 | originalPane.Title = originalTitle | 1863 | originalPane.Title = originalTitle | ||
| 1794 | 1864 | ||||
| 1865 | + | // Create child node for the original pane | |||
| 1795 | originalPaneChild := &entity.PaneNode{ | 1866 | originalPaneChild := &entity.PaneNode{ | ||
| 1796 | ID: parentNode.ID + "_0", | 1867 | ID: parentNode.ID + "_0", | ||
| 1797 | Pane: originalPane, | 1868 | Pane: originalPane, | ||
| 1798 | Parent: parentNode, | 1869 | Parent: parentNode, | ||
| 1799 | } | 1870 | } | ||
| 1800 | 1871 | ||||
| 1872 | + | // Convert parentNode to stack container | |||
| 1801 | parentNode.Pane = nil | 1873 | parentNode.Pane = nil | ||
| 1802 | parentNode.IsStacked = true | 1874 | parentNode.IsStacked = true | ||
| 1875 | + | parentNode.ActiveStackIndex = 0 | |||
| 1803 | parentNode.Children = []*entity.PaneNode{originalPaneChild} | 1876 | parentNode.Children = []*entity.PaneNode{originalPaneChild} | ||
| 1804 | 1877 | ||||
| 1805 | log.Debug(). | 1878 | log.Debug(). | ||
| @@ -1810,28 +1883,6 @@ func (c *WorkspaceCoordinator) convertToStackedNode( | |||||
| 1810 | return parentNode | 1883 | return parentNode | ||
| 1811 | } | 1884 | } | ||
| 1812 | 1885 | ||||
| 1813 | - | func (c *WorkspaceCoordinator) insertStackChild(stackNode *entity.PaneNode, pane *entity.Pane) { | |||
| 1814 | - | newChildNode := &entity.PaneNode{ | |||
| 1815 | - | ID: stackNode.ID + "_popup_" + string(pane.ID), | |||
| 1816 | - | Pane: pane, | |||
| 1817 | - | Parent: stackNode, | |||
| 1818 | - | } | |||
| 1819 | - | ||||
| 1820 | - | currentActiveIndex := stackNode.ActiveStackIndex | |||
| 1821 | - | if currentActiveIndex < 0 || currentActiveIndex >= len(stackNode.Children) { | |||
| 1822 | - | currentActiveIndex = len(stackNode.Children) - 1 | |||
| 1823 | - | if currentActiveIndex < 0 { | |||
| 1824 | - | currentActiveIndex = -1 | |||
| 1825 | - | } | |||
| 1826 | - | } | |||
| 1827 | - | insertIndex := currentActiveIndex + 1 | |||
| 1828 | - | ||||
| 1829 | - | stackNode.Children = append(stackNode.Children, nil) | |||
| 1830 | - | copy(stackNode.Children[insertIndex+1:], stackNode.Children[insertIndex:]) | |||
| 1831 | - | stackNode.Children[insertIndex] = newChildNode | |||
| 1832 | - | stackNode.ActiveStackIndex = insertIndex | |||
| 1833 | - | } | |||
| 1834 | - | ||||
| 1835 | func (c *WorkspaceCoordinator) attachPopupPaneView( | 1886 | func (c *WorkspaceCoordinator) attachPopupPaneView( | ||
| 1836 | ctx context.Context, | 1887 | ctx context.Context, | ||
| 1837 | input InsertPopupInput, | 1888 | input InsertPopupInput, | ||
| @@ -1933,6 +1984,7 @@ func (c *WorkspaceCoordinator) ConsumeOrExpelPane(ctx context.Context, direction | |||||
| 1933 | log.Warn().Err(err).Msg("failed to rebuild workspace view") | 1984 | log.Warn().Err(err).Msg("failed to rebuild workspace view") | ||
| 1934 | } | 1985 | } | ||
| 1935 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | 1986 | c.contentCoord.AttachToWorkspace(ctx, ws, wsView) | ||
| 1987 | + | c.SetupStackedPaneCallbacks(ctx, ws, wsView) | |||
| 1936 | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | 1988 | if err := wsView.SetActivePaneID(ws.ActivePaneID); err != nil { | ||
| 1937 | log.Warn().Err(err).Msg("failed to set active pane in workspace view") | 1989 | log.Warn().Err(err).Msg("failed to set active pane in workspace view") | ||
| 1938 | } | 1990 | } | ||