detached 43d4a4d

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 {
4444 findBarPaneID entity.PaneID // Which pane has the find bar
4545 findBarCfg FindBarConfig // Stored config for creating find bars
4646
47- workspace *entity.Workspace
48- paneViews map[entity.PaneID]*PaneView
49- activePaneID entity.PaneID
47+ workspace *entity.Workspace
48+ paneViews map[entity.PaneID]*PaneView
5049
5150 onPaneFocused func(paneID entity.PaneID)
5251
@@ -185,19 +184,22 @@ func (wv *WorkspaceView) SetActivePaneID(paneID entity.PaneID) error {
185184 }
186185
187186 // setActivePaneIDInternal updates active pane without locking.
187+// Updates the domain model as the single source of truth.
188188 func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error {
189+ currentActiveID := wv.getActivePaneIDInternal()
190+
189191 // Destroy omnibox if active pane is changing
190- if wv.activePaneID != paneID && wv.omnibox != nil {
192+ if currentActiveID != paneID && wv.omnibox != nil {
191193 wv.hideOmniboxInternal()
192194 }
193195 // Destroy find bar if active pane is changing
194- if wv.activePaneID != paneID && wv.findBar != nil {
196+ if currentActiveID != paneID && wv.findBar != nil {
195197 wv.hideFindBarInternal()
196198 }
197199
198200 // 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 {
201203 oldPV.SetActive(false)
202204 }
203205 }
@@ -209,17 +211,31 @@ func (wv *WorkspaceView) setActivePaneIDInternal(paneID entity.PaneID) error {
209211 }
210212
211213 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+ }
213219
214220 return nil
215221 }
216222
217223 // GetActivePaneID returns the ID of the currently active pane.
224+// Reads from the domain model as the single source of truth.
218225 func (wv *WorkspaceView) GetActivePaneID() entity.PaneID {
219226 wv.mu.RLock()
220227 defer wv.mu.RUnlock()
221228
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
223239 }
224240
225241 // GetPaneView returns the PaneView for a given pane ID.
@@ -469,9 +485,10 @@ func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) {
469485 }
470486
471487 // Get the active pane view
472- pv := wv.paneViews[wv.activePaneID]
488+ activePaneID := wv.getActivePaneIDInternal()
489+ pv := wv.paneViews[activePaneID]
473490 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")
475492 return
476493 }
477494
@@ -502,12 +519,12 @@ func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) {
502519 // Store references
503520 wv.omnibox = omnibox
504521 wv.omniboxWidget = omniboxWidget
505- wv.omniboxPaneID = wv.activePaneID
522+ wv.omniboxPaneID = activePaneID
506523
507524 // Show the omnibox
508525 omnibox.Show(ctx, query)
509526
510- wv.logger.Debug().Str("paneID", string(wv.activePaneID)).Msg("omnibox shown")
527+ wv.logger.Debug().Str("paneID", string(activePaneID)).Msg("omnibox shown")
511528 }
512529
513530 // HideOmnibox hides and destroys the current omnibox.
@@ -551,9 +568,10 @@ func (wv *WorkspaceView) ShowFindBar(ctx context.Context) {
551568 return
552569 }
553570
554- pv := wv.paneViews[wv.activePaneID]
571+ activePaneID := wv.getActivePaneIDInternal()
572+ pv := wv.paneViews[activePaneID]
555573 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")
557575 return
558576 }
559577
@@ -576,17 +594,17 @@ func (wv *WorkspaceView) ShowFindBar(ctx context.Context) {
576594 pv.AddOverlayWidget(findBarWidget)
577595
578596 if cfg.GetFindController != nil {
579- controller := cfg.GetFindController(wv.activePaneID)
597+ controller := cfg.GetFindController(activePaneID)
580598 findBar.SetFindController(controller)
581599 }
582600
583601 wv.findBar = findBar
584602 wv.findBarWidget = findBarWidget
585- wv.findBarPaneID = wv.activePaneID
603+ wv.findBarPaneID = activePaneID
586604
587605 findBar.Show()
588606
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")
590608 }
591609
592610 // HideFindBar hides and destroys the current find bar.
@@ -663,5 +681,5 @@ func (wv *WorkspaceView) IsOmniboxVisible() bool {
663681 func (wv *WorkspaceView) GetActivePaneView() *PaneView {
664682 wv.mu.RLock()
665683 defer wv.mu.RUnlock()
666- return wv.paneViews[wv.activePaneID]
684+ return wv.paneViews[wv.getActivePaneIDInternal()]
667685 }
@@ -44,9 +44,8 @@ type WorkspaceView struct {
44 findBarPaneID entity.PaneID // Which pane has the find bar44 findBarPaneID entity.PaneID // Which pane has the find bar
45 findBarCfg FindBarConfig // Stored config for creating find bars45 findBarCfg FindBarConfig // Stored config for creating find bars
46 46
47- workspace *entity.Workspace47+ workspace *entity.Workspace
48- paneViews map[entity.PaneID]*PaneView48+ 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 changing191 // 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 changing195 // 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 pane200 // 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 = paneID214+
215+ // Update domain model - single source of truth
216+ if wv.workspace != nil {
217+ wv.workspace.ActivePaneID = paneID
218+ }
213 219
214 return nil220 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.activePaneID229+ 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 view487 // 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 return492 return
476 }493 }
477 494
@@ -502,12 +519,12 @@ func (wv *WorkspaceView) ShowOmnibox(ctx context.Context, query string) {
502 // Store references519 // Store references
503 wv.omnibox = omnibox520 wv.omnibox = omnibox
504 wv.omniboxWidget = omniboxWidget521 wv.omniboxWidget = omniboxWidget
505- wv.omniboxPaneID = wv.activePaneID522+ wv.omniboxPaneID = activePaneID
506 523
507 // Show the omnibox524 // 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 return568 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 return575 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 = findBar601 wv.findBar = findBar
584 wv.findBarWidget = findBarWidget602 wv.findBarWidget = findBarWidget
585- wv.findBarPaneID = wv.activePaneID603+ 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(
266266 oldPaneView.SetActive(false)
267267 }
268268
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")
273283
274284 // 3. Create new PaneView for the new pane (without WebView - will attach later)
275285 newPaneView := component.NewPaneView(factory, output.NewPaneNode.Pane.ID, nil)
@@ -279,34 +289,94 @@ func (c *WorkspaceCoordinator) doIncrementalStackSplit(
279289 newStackedView := layout.NewStackedView(factory)
280290 newStackedView.AddPane(ctx, output.NewPaneNode.Pane.Title, "", newPaneView.Widget())
281291
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+ }
288315 } 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+ }
293320
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+ }
296346
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
298372 wsView.RegisterPaneView(output.NewPaneNode.Pane.ID, newPaneView)
299373 newPaneView.SetActive(true)
300374
301- tr := wsView.TreeRenderer()
302375 if tr != nil {
303- // Register the new pane's StackedView mapping
304376 tr.RegisterPaneInStack(string(output.NewPaneNode.Pane.ID), newStackedView)
305- // Register the split node widget
306- tr.RegisterWidget(output.ParentNode.ID, splitView.Widget())
307377 }
308378
309- // 8. Attach WebView only for the new pane
379+ // 7. Attach WebView only for the new pane
310380 wv, err := c.contentCoord.EnsureWebView(ctx, output.NewPaneNode.Pane.ID)
311381 if err != nil {
312382 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 container269+ // 2. Determine if this is a root split or non-root split
270- // Clear rootWidget ref first so SetRootWidgetDirect won't double-remove270+ // 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 placement292+ tr := wsView.TreeRenderer()
283- var splitView *layout.SplitView293+
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 right295+ 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 view321+ 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 it347+ // 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 pane379+ // 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")

Keyboard shortcuts

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

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

Branches 8

Tags 42

↑↓ moveenter openesc close