fix(ui): use domain as single source of truth for activePaneID + fix nested stack split
- commit
- 43d4a4d4e799d283e14717e52a13f68f72e5d6ae
- author
- bnema · · 2025-12-21 21:12 UTC
- parent
- 0bdd398
- signature
- unsigned
2 files · +129 -41
M internal/ui/component/workspace_view.go +37 -19
| @@ -44,9 +44,8 @@ type WorkspaceView struct { | |||
| 44 | 44 | findBarPaneID entity.PaneID // Which pane has the find bar | |
| 45 | 45 | findBarCfg FindBarConfig // Stored config for creating find bars | |
| 46 | 46 | ||
| 47 | - | workspace *entity.Workspace | |
| 48 | - | paneViews map[entity.PaneID]*PaneView | |
| 49 | - | activePaneID entity.PaneID | |
| 47 | + | workspace *entity.Workspace | |
| 48 | + | paneViews map[entity.PaneID]*PaneView | |
| 50 | 49 | ||
| 51 | 50 | onPaneFocused func(paneID entity.PaneID) | |
| 52 | 51 | ||
| @@ -185,19 +184,22 @@ func (wv *WorkspaceView) SetActivePaneID(paneID entity.PaneID) error { | |||
| 185 | 184 | } | |
| 186 | 185 | ||
| 187 | 186 | // setActivePaneIDInternal updates active pane without locking. | |
| 187 | + | // Updates the domain model as the single source of truth. | |
| 188 | 188 | func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error { | |
| 189 | + | currentActiveID := wv.getActivePaneIDInternal() | |
| 190 | + | ||
| 189 | 191 | // Destroy omnibox if active pane is changing | |
| 190 | - | if wv.activePaneID != paneID && wv.omnibox != nil { | |
| 192 | + | if currentActiveID != paneID && wv.omnibox != nil { | |
| 191 | 193 | wv.hideOmniboxInternal() | |
| 192 | 194 | } | |
| 193 | 195 | // Destroy find bar if active pane is changing | |
| 194 | - | if wv.activePaneID != paneID && wv.findBar != nil { | |
| 196 | + | if currentActiveID != paneID && wv.findBar != nil { | |
| 195 | 197 | wv.hideFindBarInternal() | |
| 196 | 198 | } | |
| 197 | 199 | ||
| 198 | 200 | // Deactivate current active pane | |
| 199 | - | if wv.activePaneID != "" { | |
| 200 | - | if oldPV, ok := wv.paneViews[wv.activePaneID]; ok { | |
| 201 | + | if currentActiveID != "" { | |
| 202 | + | if oldPV, ok := wv.paneViews[currentActiveID]; ok { | |
| 201 | 203 | oldPV.SetActive(false) | |
| 202 | 204 | } | |
| 203 | 205 | } | |
| @@ -209,17 +211,31 @@ func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error { | |||
| 209 | 211 | } | |
| 210 | 212 | ||
| 211 | 213 | newPV.SetActive(true) | |
| 212 | - | wv.activePaneID = paneID | |
| 214 | + | ||
| 215 | + | // Update domain model - single source of truth | |
| 216 | + | if wv.workspace != nil { | |
| 217 | + | wv.workspace.ActivePaneID = paneID | |
| 218 | + | } | |
| 213 | 219 | ||
| 214 | 220 | return nil | |
| 215 | 221 | } | |
| 216 | 222 | ||
| 217 | 223 | // GetActivePaneID returns the ID of the currently active pane. | |
| 224 | + | // Reads from the domain model as the single source of truth. | |
| 218 | 225 | func (wv *WorkspaceView) GetActivePaneID() entity.PaneID { | |
| 219 | 226 | wv.mu.RLock() | |
| 220 | 227 | defer wv.mu.RUnlock() | |
| 221 | 228 | ||
| 222 | - | return wv.activePaneID | |
| 229 | + | return wv.getActivePaneIDInternal() | |
| 230 | + | } | |
| 231 | + | ||
| 232 | + | // getActivePaneIDInternal returns the active pane ID without locking. | |
| 233 | + | // Must be called with at least a read lock held. | |
| 234 | + | func (wv *WorkspaceView) getActivePaneIDInternal() entity.PaneID { | |
| 235 | + | if wv.workspace == nil { | |
| 236 | + | return "" | |
| 237 | + | } | |
| 238 | + | return wv.workspace.ActivePaneID | |
| 223 | 239 | } | |
| 224 | 240 | ||
| 225 | 241 | // GetPaneView returns the PaneView for a given pane ID. | |
| @@ -469,9 +485,10 @@ func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) { | |||
| 469 | 485 | } | |
| 470 | 486 | ||
| 471 | 487 | // Get the active pane view | |
| 472 | - | pv := wv.paneViews[wv.activePaneID] | |
| 488 | + | activePaneID := wv.getActivePaneIDInternal() | |
| 489 | + | pv := wv.paneViews[activePaneID] | |
| 473 | 490 | if pv == nil { | |
| 474 | - | wv.logger.Warn().Str("paneID", string(wv.activePaneID)).Msg("cannot show omnibox: active pane not found") | |
| 491 | + | wv.logger.Warn().Str("paneID", string(activePaneID)).Msg("cannot show omnibox: active pane not found") | |
| 475 | 492 | return | |
| 476 | 493 | } | |
| 477 | 494 | ||
| @@ -502,12 +519,12 @@ func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) { | |||
| 502 | 519 | // Store references | |
| 503 | 520 | wv.omnibox = omnibox | |
| 504 | 521 | wv.omniboxWidget = omniboxWidget | |
| 505 | - | wv.omniboxPaneID = wv.activePaneID | |
| 522 | + | wv.omniboxPaneID = activePaneID | |
| 506 | 523 | ||
| 507 | 524 | // Show the omnibox | |
| 508 | 525 | omnibox.Show(ctx, query) | |
| 509 | 526 | ||
| 510 | - | wv.logger.Debug().Str("paneID", string(wv.activePaneID)).Msg("omnibox shown") | |
| 527 | + | wv.logger.Debug().Str("paneID", string(activePaneID)).Msg("omnibox shown") | |
| 511 | 528 | } | |
| 512 | 529 | ||
| 513 | 530 | // HideOmnibox hides and destroys the current omnibox. | |
| @@ -551,9 +568,10 @@ func (wv *WorkspaceView) ShowFindBar(ctx context.Context) { | |||
| 551 | 568 | return | |
| 552 | 569 | } | |
| 553 | 570 | ||
| 554 | - | pv := wv.paneViews[wv.activePaneID] | |
| 571 | + | activePaneID := wv.getActivePaneIDInternal() | |
| 572 | + | pv := wv.paneViews[activePaneID] | |
| 555 | 573 | if pv == nil { | |
| 556 | - | wv.logger.Warn().Str("paneID", string(wv.activePaneID)).Msg("cannot show find bar: active pane not found") | |
| 574 | + | wv.logger.Warn().Str("paneID", string(activePaneID)).Msg("cannot show find bar: active pane not found") | |
| 557 | 575 | return | |
| 558 | 576 | } | |
| 559 | 577 | ||
| @@ -576,17 +594,17 @@ func (wv *WorkspaceView) ShowFindBar(ctx context.Context) { | |||
| 576 | 594 | pv.AddOverlayWidget(findBarWidget) | |
| 577 | 595 | ||
| 578 | 596 | if cfg.GetFindController != nil { | |
| 579 | - | controller := cfg.GetFindController(wv.activePaneID) | |
| 597 | + | controller := cfg.GetFindController(activePaneID) | |
| 580 | 598 | findBar.SetFindController(controller) | |
| 581 | 599 | } | |
| 582 | 600 | ||
| 583 | 601 | wv.findBar = findBar | |
| 584 | 602 | wv.findBarWidget = findBarWidget | |
| 585 | - | wv.findBarPaneID = wv.activePaneID | |
| 603 | + | wv.findBarPaneID = activePaneID | |
| 586 | 604 | ||
| 587 | 605 | findBar.Show() | |
| 588 | 606 | ||
| 589 | - | wv.logger.Debug().Str("paneID", string(wv.activePaneID)).Msg("find bar shown") | |
| 607 | + | wv.logger.Debug().Str("paneID", string(activePaneID)).Msg("find bar shown") | |
| 590 | 608 | } | |
| 591 | 609 | ||
| 592 | 610 | // HideFindBar hides and destroys the current find bar. | |
| @@ -663,5 +681,5 @@ func (wv *WorkspaceView) IsOmniboxVisible() bool { | |||
| 663 | 681 | func (wv *WorkspaceView) GetActivePaneView() *PaneView { | |
| 664 | 682 | wv.mu.RLock() | |
| 665 | 683 | defer wv.mu.RUnlock() | |
| 666 | - | return wv.paneViews[wv.activePaneID] | |
| 684 | + | return wv.paneViews[wv.getActivePaneIDInternal()] | |
| 667 | 685 | } | |
| @@ -44,9 +44,8 @@ type WorkspaceView struct { | |||||
| 44 | findBarPaneID entity.PaneID // Which pane has the find bar | 44 | findBarPaneID entity.PaneID // Which pane has the find bar | ||
| 45 | findBarCfg FindBarConfig // Stored config for creating find bars | 45 | findBarCfg FindBarConfig // Stored config for creating find bars | ||
| 46 | 46 | ||||
| 47 | - | workspace *entity.Workspace | 47 | + | workspace *entity.Workspace |
| 48 | - | paneViews map[entity.PaneID]*PaneView | 48 | + | paneViews map[entity.PaneID]*PaneView |
| 49 | - | activePaneID entity.PaneID | |||
| 50 | 49 | ||||
| 51 | onPaneFocused func(paneID entity.PaneID) | 50 | onPaneFocused func(paneID entity.PaneID) | ||
| 52 | 51 | ||||
| @@ -185,19 +184,22 @@ func (wv *WorkspaceView) SetActivePaneID(paneID entity.PaneID) error { | |||||
| 185 | } | 184 | } | ||
| 186 | 185 | ||||
| 187 | // setActivePaneIDInternal updates active pane without locking. | 186 | // setActivePaneIDInternal updates active pane without locking. | ||
| 187 | + | // Updates the domain model as the single source of truth. | |||
| 188 | func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error { | 188 | func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error { | ||
| 189 | + | currentActiveID := wv.getActivePaneIDInternal() | |||
| 190 | + | ||||
| 189 | // Destroy omnibox if active pane is changing | 191 | // Destroy omnibox if active pane is changing | ||
| 190 | - | if wv.activePaneID != paneID && wv.omnibox != nil { | 192 | + | if currentActiveID != paneID && wv.omnibox != nil { |
| 191 | wv.hideOmniboxInternal() | 193 | wv.hideOmniboxInternal() | ||
| 192 | } | 194 | } | ||
| 193 | // Destroy find bar if active pane is changing | 195 | // Destroy find bar if active pane is changing | ||
| 194 | - | if wv.activePaneID != paneID && wv.findBar != nil { | 196 | + | if currentActiveID != paneID && wv.findBar != nil { |
| 195 | wv.hideFindBarInternal() | 197 | wv.hideFindBarInternal() | ||
| 196 | } | 198 | } | ||
| 197 | 199 | ||||
| 198 | // Deactivate current active pane | 200 | // Deactivate current active pane | ||
| 199 | - | if wv.activePaneID != "" { | 201 | + | if currentActiveID != "" { |
| 200 | - | if oldPV, ok := wv.paneViews[wv.activePaneID]; ok { | 202 | + | if oldPV, ok := wv.paneViews[currentActiveID]; ok { |
| 201 | oldPV.SetActive(false) | 203 | oldPV.SetActive(false) | ||
| 202 | } | 204 | } | ||
| 203 | } | 205 | } | ||
| @@ -209,17 +211,31 @@ func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error { | |||||
| 209 | } | 211 | } | ||
| 210 | 212 | ||||
| 211 | newPV.SetActive(true) | 213 | newPV.SetActive(true) | ||
| 212 | - | wv.activePaneID = paneID | 214 | + | |
| 215 | + | // Update domain model - single source of truth | |||
| 216 | + | if wv.workspace != nil { | |||
| 217 | + | wv.workspace.ActivePaneID = paneID | |||
| 218 | + | } | |||
| 213 | 219 | ||||
| 214 | return nil | 220 | return nil | ||
| 215 | } | 221 | } | ||
| 216 | 222 | ||||
| 217 | // GetActivePaneID returns the ID of the currently active pane. | 223 | // GetActivePaneID returns the ID of the currently active pane. | ||
| 224 | + | // Reads from the domain model as the single source of truth. | |||
| 218 | func (wv *WorkspaceView) GetActivePaneID() entity.PaneID { | 225 | func (wv *WorkspaceView) GetActivePaneID() entity.PaneID { | ||
| 219 | wv.mu.RLock() | 226 | wv.mu.RLock() | ||
| 220 | defer wv.mu.RUnlock() | 227 | defer wv.mu.RUnlock() | ||
| 221 | 228 | ||||
| 222 | - | return wv.activePaneID | 229 | + | return wv.getActivePaneIDInternal() |
| 230 | + | } | |||
| 231 | + | ||||
| 232 | + | // getActivePaneIDInternal returns the active pane ID without locking. | |||
| 233 | + | // Must be called with at least a read lock held. | |||
| 234 | + | func (wv *WorkspaceView) getActivePaneIDInternal() entity.PaneID { | |||
| 235 | + | if wv.workspace == nil { | |||
| 236 | + | return "" | |||
| 237 | + | } | |||
| 238 | + | return wv.workspace.ActivePaneID | |||
| 223 | } | 239 | } | ||
| 224 | 240 | ||||
| 225 | // GetPaneView returns the PaneView for a given pane ID. | 241 | // GetPaneView returns the PaneView for a given pane ID. | ||
| @@ -469,9 +485,10 @@ func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) { | |||||
| 469 | } | 485 | } | ||
| 470 | 486 | ||||
| 471 | // Get the active pane view | 487 | // Get the active pane view | ||
| 472 | - | pv := wv.paneViews[wv.activePaneID] | 488 | + | activePaneID := wv.getActivePaneIDInternal() |
| 489 | + | pv := wv.paneViews[activePaneID] | |||
| 473 | if pv == nil { | 490 | if pv == nil { | ||
| 474 | - | wv.logger.Warn().Str("paneID", string(wv.activePaneID)).Msg("cannot show omnibox: active pane not found") | 491 | + | wv.logger.Warn().Str("paneID", string(activePaneID)).Msg("cannot show omnibox: active pane not found") |
| 475 | return | 492 | return | ||
| 476 | } | 493 | } | ||
| 477 | 494 | ||||
| @@ -502,12 +519,12 @@ func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) { | |||||
| 502 | // Store references | 519 | // Store references | ||
| 503 | wv.omnibox = omnibox | 520 | wv.omnibox = omnibox | ||
| 504 | wv.omniboxWidget = omniboxWidget | 521 | wv.omniboxWidget = omniboxWidget | ||
| 505 | - | wv.omniboxPaneID = wv.activePaneID | 522 | + | wv.omniboxPaneID = activePaneID |
| 506 | 523 | ||||
| 507 | // Show the omnibox | 524 | // Show the omnibox | ||
| 508 | omnibox.Show(ctx, query) | 525 | omnibox.Show(ctx, query) | ||
| 509 | 526 | ||||
| 510 | - | wv.logger.Debug().Str("paneID", string(wv.activePaneID)).Msg("omnibox shown") | 527 | + | wv.logger.Debug().Str("paneID", string(activePaneID)).Msg("omnibox shown") |
| 511 | } | 528 | } | ||
| 512 | 529 | ||||
| 513 | // HideOmnibox hides and destroys the current omnibox. | 530 | // HideOmnibox hides and destroys the current omnibox. | ||
| @@ -551,9 +568,10 @@ func (wv *WorkspaceView) ShowFindBar(ctx context.Context) { | |||||
| 551 | return | 568 | return | ||
| 552 | } | 569 | } | ||
| 553 | 570 | ||||
| 554 | - | pv := wv.paneViews[wv.activePaneID] | 571 | + | activePaneID := wv.getActivePaneIDInternal() |
| 572 | + | pv := wv.paneViews[activePaneID] | |||
| 555 | if pv == nil { | 573 | if pv == nil { | ||
| 556 | - | wv.logger.Warn().Str("paneID", string(wv.activePaneID)).Msg("cannot show find bar: active pane not found") | 574 | + | wv.logger.Warn().Str("paneID", string(activePaneID)).Msg("cannot show find bar: active pane not found") |
| 557 | return | 575 | return | ||
| 558 | } | 576 | } | ||
| 559 | 577 | ||||
| @@ -576,17 +594,17 @@ func (wv *WorkspaceView) ShowFindBar(ctx context.Context) { | |||||
| 576 | pv.AddOverlayWidget(findBarWidget) | 594 | pv.AddOverlayWidget(findBarWidget) | ||
| 577 | 595 | ||||
| 578 | if cfg.GetFindController != nil { | 596 | if cfg.GetFindController != nil { | ||
| 579 | - | controller := cfg.GetFindController(wv.activePaneID) | 597 | + | controller := cfg.GetFindController(activePaneID) |
| 580 | findBar.SetFindController(controller) | 598 | findBar.SetFindController(controller) | ||
| 581 | } | 599 | } | ||
| 582 | 600 | ||||
| 583 | wv.findBar = findBar | 601 | wv.findBar = findBar | ||
| 584 | wv.findBarWidget = findBarWidget | 602 | wv.findBarWidget = findBarWidget | ||
| 585 | - | wv.findBarPaneID = wv.activePaneID | 603 | + | wv.findBarPaneID = activePaneID |
| 586 | 604 | ||||
| 587 | findBar.Show() | 605 | findBar.Show() | ||
| 588 | 606 | ||||
| 589 | - | wv.logger.Debug().Str("paneID", string(wv.activePaneID)).Msg("find bar shown") | 607 | + | wv.logger.Debug().Str("paneID", string(activePaneID)).Msg("find bar shown") |
| 590 | } | 608 | } | ||
| 591 | 609 | ||||
| 592 | // HideFindBar hides and destroys the current find bar. | 610 | // HideFindBar hides and destroys the current find bar. | ||
| @@ -663,5 +681,5 @@ func (wv *WorkspaceView) IsOmniboxVisible() bool { | |||||
| 663 | func (wv *WorkspaceView) GetActivePaneView() *PaneView { | 681 | func (wv *WorkspaceView) GetActivePaneView() *PaneView { | ||
| 664 | wv.mu.RLock() | 682 | wv.mu.RLock() | ||
| 665 | defer wv.mu.RUnlock() | 683 | defer wv.mu.RUnlock() | ||
| 666 | - | return wv.paneViews[wv.activePaneID] | 684 | + | return wv.paneViews[wv.getActivePaneIDInternal()] |
| 667 | } | 685 | } | ||
M internal/ui/coordinator/workspace.go +92 -22
| @@ -266,10 +266,20 @@ func (c *WorkspaceCoordinator) doIncrementalStackSplit( | |||
| 266 | 266 | oldPaneView.SetActive(false) | |
| 267 | 267 | } | |
| 268 | 268 | ||
| 269 | - | // 2. Unparent the existing stack widget from the workspace container | |
| 270 | - | // Clear rootWidget ref first so SetRootWidgetDirect won't double-remove | |
| 271 | - | wsView.ClearRootWidgetRef() | |
| 272 | - | wsView.Container().Remove(existingStackWidget) | |
| 269 | + | // 2. Determine if this is a root split or non-root split | |
| 270 | + | // The grandparent of the new split node tells us where the stack currently sits | |
| 271 | + | grandparentNode := output.ParentNode.Parent | |
| 272 | + | isRootSplit := grandparentNode == nil | |
| 273 | + | ||
| 274 | + | log.Debug(). | |
| 275 | + | Bool("is_root_split", isRootSplit). | |
| 276 | + | Str("grandparent_id", func() string { | |
| 277 | + | if grandparentNode != nil { | |
| 278 | + | return grandparentNode.ID | |
| 279 | + | } | |
| 280 | + | return nilString | |
| 281 | + | }()). | |
| 282 | + | Msg("stack split: determined split type") | |
| 273 | 283 | ||
| 274 | 284 | // 3. Create new PaneView for the new pane (without WebView - will attach later) | |
| 275 | 285 | newPaneView := component.NewPaneView(factory, output.NewPaneNode.Pane.ID, nil) | |
| @@ -279,34 +289,94 @@ func (c *WorkspaceCoordinator) doIncrementalStackSplit( | |||
| 279 | 289 | newStackedView := layout.NewStackedView(factory) | |
| 280 | 290 | newStackedView.AddPane(ctx, output.NewPaneNode.Pane.Title, "", newPaneView.Widget()) | |
| 281 | 291 | ||
| 282 | - | // 5. Create a SplitView with the appropriate orientation and child placement | |
| 283 | - | var splitView *layout.SplitView | |
| 284 | - | if direction == usecase.SplitLeft { | |
| 285 | - | // New pane goes on the left, existing stack on the right | |
| 286 | - | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | |
| 287 | - | newStackedView.Widget(), existingStackWidget, output.SplitRatio) | |
| 292 | + | tr := wsView.TreeRenderer() | |
| 293 | + | ||
| 294 | + | // 5. Handle root vs non-root split differently | |
| 295 | + | if isRootSplit { | |
| 296 | + | // Root split: stack is direct child of container | |
| 297 | + | wsView.ClearRootWidgetRef() | |
| 298 | + | wsView.Container().Remove(existingStackWidget) | |
| 299 | + | ||
| 300 | + | // Create a SplitView with the appropriate orientation and child placement | |
| 301 | + | var splitView *layout.SplitView | |
| 302 | + | if direction == usecase.SplitLeft { | |
| 303 | + | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | |
| 304 | + | newStackedView.Widget(), existingStackWidget, output.SplitRatio) | |
| 305 | + | } else { | |
| 306 | + | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | |
| 307 | + | existingStackWidget, newStackedView.Widget(), output.SplitRatio) | |
| 308 | + | } | |
| 309 | + | ||
| 310 | + | wsView.SetRootWidgetDirect(splitView.Widget()) | |
| 311 | + | ||
| 312 | + | if tr != nil { | |
| 313 | + | tr.RegisterWidget(output.ParentNode.ID, splitView.Widget()) | |
| 314 | + | } | |
| 288 | 315 | } else { | |
| 289 | - | // New pane goes on the right, existing stack on the left (SplitRight) | |
| 290 | - | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | |
| 291 | - | existingStackWidget, newStackedView.Widget(), output.SplitRatio) | |
| 292 | - | } | |
| 316 | + | // Non-root split: stack is inside another split, need to replace in grandparent | |
| 317 | + | if tr == nil { | |
| 318 | + | return fmt.Errorf("tree renderer not available for non-root stack split") | |
| 319 | + | } | |
| 293 | 320 | ||
| 294 | - | // 6. Replace the root widget in the workspace view | |
| 295 | - | wsView.SetRootWidgetDirect(splitView.Widget()) | |
| 321 | + | grandparentWidget := tr.Lookup(grandparentNode.ID) | |
| 322 | + | if grandparentWidget == nil { | |
| 323 | + | log.Warn().Str("grandparent_id", grandparentNode.ID).Msg("grandparent widget not found") | |
| 324 | + | return fmt.Errorf("grandparent widget not found") | |
| 325 | + | } | |
| 326 | + | ||
| 327 | + | panedWidget, ok := grandparentWidget.(layout.PanedWidget) | |
| 328 | + | if !ok { | |
| 329 | + | log.Warn().Msg("grandparent widget is not a PanedWidget") | |
| 330 | + | return fmt.Errorf("grandparent is not a PanedWidget") | |
| 331 | + | } | |
| 332 | + | ||
| 333 | + | // Determine if the stack's parent (the new split node) is start or end child | |
| 334 | + | isStartChild := grandparentNode.Left() == output.ParentNode | |
| 335 | + | ||
| 336 | + | log.Debug(). | |
| 337 | + | Bool("is_start_child", isStartChild). | |
| 338 | + | Msg("stack split: determined position in grandparent") | |
| 339 | + | ||
| 340 | + | // Clear the position first to unparent the stack widget | |
| 341 | + | if isStartChild { | |
| 342 | + | panedWidget.SetStartChild(nil) | |
| 343 | + | } else { | |
| 344 | + | panedWidget.SetEndChild(nil) | |
| 345 | + | } | |
| 296 | 346 | ||
| 297 | - | // 7. Register the new pane in tracking maps and activate it | |
| 347 | + | // Create the new split view | |
| 348 | + | var splitView *layout.SplitView | |
| 349 | + | if direction == usecase.SplitLeft { | |
| 350 | + | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | |
| 351 | + | newStackedView.Widget(), existingStackWidget, output.SplitRatio) | |
| 352 | + | } else { | |
| 353 | + | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | |
| 354 | + | existingStackWidget, newStackedView.Widget(), output.SplitRatio) | |
| 355 | + | } | |
| 356 | + | ||
| 357 | + | // Put the new split view in the grandparent's slot | |
| 358 | + | if isStartChild { | |
| 359 | + | panedWidget.SetStartChild(splitView.Widget()) | |
| 360 | + | } else { | |
| 361 | + | panedWidget.SetEndChild(splitView.Widget()) | |
| 362 | + | } | |
| 363 | + | ||
| 364 | + | tr.RegisterWidget(output.ParentNode.ID, splitView.Widget()) | |
| 365 | + | ||
| 366 | + | log.Debug(). | |
| 367 | + | Bool("was_start_child", isStartChild). | |
| 368 | + | Msg("stack split: replaced stack in grandparent with new split view") | |
| 369 | + | } | |
| 370 | + | ||
| 371 | + | // 6. Register the new pane in tracking maps and activate it | |
| 298 | 372 | wsView.RegisterPaneView(output.NewPaneNode.Pane.ID, newPaneView) | |
| 299 | 373 | newPaneView.SetActive(true) | |
| 300 | 374 | ||
| 301 | - | tr := wsView.TreeRenderer() | |
| 302 | 375 | if tr != nil { | |
| 303 | - | // Register the new pane's StackedView mapping | |
| 304 | 376 | tr.RegisterPaneInStack(string(output.NewPaneNode.Pane.ID), newStackedView) | |
| 305 | - | // Register the split node widget | |
| 306 | - | tr.RegisterWidget(output.ParentNode.ID, splitView.Widget()) | |
| 307 | 377 | } | |
| 308 | 378 | ||
| 309 | - | // 8. Attach WebView only for the new pane | |
| 379 | + | // 7. Attach WebView only for the new pane | |
| 310 | 380 | wv, err := c.contentCoord.EnsureWebView(ctx, output.NewPaneNode.Pane.ID) | |
| 311 | 381 | if err != nil { | |
| 312 | 382 | log.Warn().Err(err).Str("pane_id", string(output.NewPaneNode.Pane.ID)).Msg("failed to ensure webview for new pane") | |
| @@ -266,10 +266,20 @@ func (c *WorkspaceCoordinator) doIncrementalStackSplit( | |||||
| 266 | oldPaneView.SetActive(false) | 266 | oldPaneView.SetActive(false) | ||
| 267 | } | 267 | } | ||
| 268 | 268 | ||||
| 269 | - | // 2. Unparent the existing stack widget from the workspace container | 269 | + | // 2. Determine if this is a root split or non-root split |
| 270 | - | // Clear rootWidget ref first so SetRootWidgetDirect won't double-remove | 270 | + | // The grandparent of the new split node tells us where the stack currently sits |
| 271 | - | wsView.ClearRootWidgetRef() | 271 | + | grandparentNode := output.ParentNode.Parent |
| 272 | - | wsView.Container().Remove(existingStackWidget) | 272 | + | isRootSplit := grandparentNode == nil |
| 273 | + | ||||
| 274 | + | log.Debug(). | |||
| 275 | + | Bool("is_root_split", isRootSplit). | |||
| 276 | + | Str("grandparent_id", func() string { | |||
| 277 | + | if grandparentNode != nil { | |||
| 278 | + | return grandparentNode.ID | |||
| 279 | + | } | |||
| 280 | + | return nilString | |||
| 281 | + | }()). | |||
| 282 | + | Msg("stack split: determined split type") | |||
| 273 | 283 | ||||
| 274 | // 3. Create new PaneView for the new pane (without WebView - will attach later) | 284 | // 3. Create new PaneView for the new pane (without WebView - will attach later) | ||
| 275 | newPaneView := component.NewPaneView(factory, output.NewPaneNode.Pane.ID, nil) | 285 | newPaneView := component.NewPaneView(factory, output.NewPaneNode.Pane.ID, nil) | ||
| @@ -279,34 +289,94 @@ func (c *WorkspaceCoordinator) doIncrementalStackSplit( | |||||
| 279 | newStackedView := layout.NewStackedView(factory) | 289 | newStackedView := layout.NewStackedView(factory) | ||
| 280 | newStackedView.AddPane(ctx, output.NewPaneNode.Pane.Title, "", newPaneView.Widget()) | 290 | newStackedView.AddPane(ctx, output.NewPaneNode.Pane.Title, "", newPaneView.Widget()) | ||
| 281 | 291 | ||||
| 282 | - | // 5. Create a SplitView with the appropriate orientation and child placement | 292 | + | tr := wsView.TreeRenderer() |
| 283 | - | var splitView *layout.SplitView | 293 | + | |
| 284 | - | if direction == usecase.SplitLeft { | 294 | + | // 5. Handle root vs non-root split differently |
| 285 | - | // New pane goes on the left, existing stack on the right | 295 | + | if isRootSplit { |
| 286 | - | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | 296 | + | // Root split: stack is direct child of container |
| 287 | - | newStackedView.Widget(), existingStackWidget, output.SplitRatio) | 297 | + | wsView.ClearRootWidgetRef() |
| 298 | + | wsView.Container().Remove(existingStackWidget) | |||
| 299 | + | ||||
| 300 | + | // Create a SplitView with the appropriate orientation and child placement | |||
| 301 | + | var splitView *layout.SplitView | |||
| 302 | + | if direction == usecase.SplitLeft { | |||
| 303 | + | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | |||
| 304 | + | newStackedView.Widget(), existingStackWidget, output.SplitRatio) | |||
| 305 | + | } else { | |||
| 306 | + | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | |||
| 307 | + | existingStackWidget, newStackedView.Widget(), output.SplitRatio) | |||
| 308 | + | } | |||
| 309 | + | ||||
| 310 | + | wsView.SetRootWidgetDirect(splitView.Widget()) | |||
| 311 | + | ||||
| 312 | + | if tr != nil { | |||
| 313 | + | tr.RegisterWidget(output.ParentNode.ID, splitView.Widget()) | |||
| 314 | + | } | |||
| 288 | } else { | 315 | } else { | ||
| 289 | - | // New pane goes on the right, existing stack on the left (SplitRight) | 316 | + | // Non-root split: stack is inside another split, need to replace in grandparent |
| 290 | - | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | 317 | + | if tr == nil { |
| 291 | - | existingStackWidget, newStackedView.Widget(), output.SplitRatio) | 318 | + | return fmt.Errorf("tree renderer not available for non-root stack split") |
| 292 | - | } | 319 | + | } |
| 293 | 320 | ||||
| 294 | - | // 6. Replace the root widget in the workspace view | 321 | + | grandparentWidget := tr.Lookup(grandparentNode.ID) |
| 295 | - | wsView.SetRootWidgetDirect(splitView.Widget()) | 322 | + | if grandparentWidget == nil { |
| 323 | + | log.Warn().Str("grandparent_id", grandparentNode.ID).Msg("grandparent widget not found") | |||
| 324 | + | return fmt.Errorf("grandparent widget not found") | |||
| 325 | + | } | |||
| 326 | + | ||||
| 327 | + | panedWidget, ok := grandparentWidget.(layout.PanedWidget) | |||
| 328 | + | if !ok { | |||
| 329 | + | log.Warn().Msg("grandparent widget is not a PanedWidget") | |||
| 330 | + | return fmt.Errorf("grandparent is not a PanedWidget") | |||
| 331 | + | } | |||
| 332 | + | ||||
| 333 | + | // Determine if the stack's parent (the new split node) is start or end child | |||
| 334 | + | isStartChild := grandparentNode.Left() == output.ParentNode | |||
| 335 | + | ||||
| 336 | + | log.Debug(). | |||
| 337 | + | Bool("is_start_child", isStartChild). | |||
| 338 | + | Msg("stack split: determined position in grandparent") | |||
| 339 | + | ||||
| 340 | + | // Clear the position first to unparent the stack widget | |||
| 341 | + | if isStartChild { | |||
| 342 | + | panedWidget.SetStartChild(nil) | |||
| 343 | + | } else { | |||
| 344 | + | panedWidget.SetEndChild(nil) | |||
| 345 | + | } | |||
| 296 | 346 | ||||
| 297 | - | // 7. Register the new pane in tracking maps and activate it | 347 | + | // Create the new split view |
| 348 | + | var splitView *layout.SplitView | |||
| 349 | + | if direction == usecase.SplitLeft { | |||
| 350 | + | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | |||
| 351 | + | newStackedView.Widget(), existingStackWidget, output.SplitRatio) | |||
| 352 | + | } else { | |||
| 353 | + | splitView = layout.NewSplitView(ctx, factory, layout.OrientationHorizontal, | |||
| 354 | + | existingStackWidget, newStackedView.Widget(), output.SplitRatio) | |||
| 355 | + | } | |||
| 356 | + | ||||
| 357 | + | // Put the new split view in the grandparent's slot | |||
| 358 | + | if isStartChild { | |||
| 359 | + | panedWidget.SetStartChild(splitView.Widget()) | |||
| 360 | + | } else { | |||
| 361 | + | panedWidget.SetEndChild(splitView.Widget()) | |||
| 362 | + | } | |||
| 363 | + | ||||
| 364 | + | tr.RegisterWidget(output.ParentNode.ID, splitView.Widget()) | |||
| 365 | + | ||||
| 366 | + | log.Debug(). | |||
| 367 | + | Bool("was_start_child", isStartChild). | |||
| 368 | + | Msg("stack split: replaced stack in grandparent with new split view") | |||
| 369 | + | } | |||
| 370 | + | ||||
| 371 | + | // 6. Register the new pane in tracking maps and activate it | |||
| 298 | wsView.RegisterPaneView(output.NewPaneNode.Pane.ID, newPaneView) | 372 | wsView.RegisterPaneView(output.NewPaneNode.Pane.ID, newPaneView) | ||
| 299 | newPaneView.SetActive(true) | 373 | newPaneView.SetActive(true) | ||
| 300 | 374 | ||||
| 301 | - | tr := wsView.TreeRenderer() | |||
| 302 | if tr != nil { | 375 | if tr != nil { | ||
| 303 | - | // Register the new pane's StackedView mapping | |||
| 304 | tr.RegisterPaneInStack(string(output.NewPaneNode.Pane.ID), newStackedView) | 376 | tr.RegisterPaneInStack(string(output.NewPaneNode.Pane.ID), newStackedView) | ||
| 305 | - | // Register the split node widget | |||
| 306 | - | tr.RegisterWidget(output.ParentNode.ID, splitView.Widget()) | |||
| 307 | } | 377 | } | ||
| 308 | 378 | ||||
| 309 | - | // 8. Attach WebView only for the new pane | 379 | + | // 7. Attach WebView only for the new pane |
| 310 | wv, err := c.contentCoord.EnsureWebView(ctx, output.NewPaneNode.Pane.ID) | 380 | wv, err := c.contentCoord.EnsureWebView(ctx, output.NewPaneNode.Pane.ID) | ||
| 311 | if err != nil { | 381 | if err != nil { | ||
| 312 | log.Warn().Err(err).Str("pane_id", string(output.NewPaneNode.Pane.ID)).Msg("failed to ensure webview for new pane") | 382 | log.Warn().Err(err).Str("pane_id", string(output.NewPaneNode.Pane.ID)).Msg("failed to ensure webview for new pane") | ||