detached 505bddb

fix(workspace): enhance widget safety and fix isStacked semantics in supporting components

commit
505bddb0b2d239dbb341e97add9569db1dcba252
author
Brice · · 2025-09-30 05:15 UTC
parent
05fb5c2
signature
unsigned
4 files · +62 -58
M internal/app/browser/stacked_panes.go +12 -3
@@ -225,8 +225,10 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo
225225 // Immediately reattach the stack wrapper to minimize visibility gap
226226 spm.reattachToParent(stackWrapperContainer, target, parent)
227227
228- // Convert target to a stacked leaf node
229- target.isStacked = true
228+ // Keep target as a regular leaf node (it's just inside a stack now)
229+ // Individual panes within a stack should NOT be marked as isStacked
230+ // Only the stack container itself has isStacked=true
231+ target.isStacked = false
230232 target.isLeaf = true
231233 // container stays the same (existingContainer)
232234 spm.wm.setTitleBar(target, titleBar)
@@ -264,10 +266,17 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo
264266 stackNode.stackedPanes = append(stackNode.stackedPanes, newLeaf)
265267
266268 // Unparent the new container before adding it to the stack
269+ // This is critical: even freshly created WebViews might have internal parent refs
267270 if newLeaf.container != nil {
268271 newLeaf.container.Execute(func(containerPtr uintptr) error {
269- if webkit.WidgetGetParent(containerPtr) != 0 {
272+ parent := webkit.WidgetGetParent(containerPtr)
273+ if parent != 0 {
274+ log.Printf("[workspace] unparenting new pane container %#x from parent %#x before stack append", containerPtr, parent)
270275 webkit.WidgetUnparent(containerPtr)
276+ // Verify unparent succeeded
277+ if finalParent := webkit.WidgetGetParent(containerPtr); finalParent != 0 {
278+ log.Printf("[workspace] WARNING: container %#x still has parent %#x after unparent", containerPtr, finalParent)
279+ }
271280 }
272281 return nil
273282 })
@@ -225,8 +225,10 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo
225 // Immediately reattach the stack wrapper to minimize visibility gap225 // Immediately reattach the stack wrapper to minimize visibility gap
226 spm.reattachToParent(stackWrapperContainer, target, parent)226 spm.reattachToParent(stackWrapperContainer, target, parent)
227 227
228- // Convert target to a stacked leaf node228+ // Keep target as a regular leaf node (it's just inside a stack now)
229- target.isStacked = true229+ // Individual panes within a stack should NOT be marked as isStacked
230+ // Only the stack container itself has isStacked=true
231+ target.isStacked = false
230 target.isLeaf = true232 target.isLeaf = true
231 // container stays the same (existingContainer)233 // container stays the same (existingContainer)
232 spm.wm.setTitleBar(target, titleBar)234 spm.wm.setTitleBar(target, titleBar)
@@ -264,10 +266,17 @@ func (spm *StackedPaneManager) convertToStackedContainer(target, newLeaf *paneNo
264 stackNode.stackedPanes = append(stackNode.stackedPanes, newLeaf)266 stackNode.stackedPanes = append(stackNode.stackedPanes, newLeaf)
265 267
266 // Unparent the new container before adding it to the stack268 // Unparent the new container before adding it to the stack
269+ // This is critical: even freshly created WebViews might have internal parent refs
267 if newLeaf.container != nil {270 if newLeaf.container != nil {
268 newLeaf.container.Execute(func(containerPtr uintptr) error {271 newLeaf.container.Execute(func(containerPtr uintptr) error {
269- if webkit.WidgetGetParent(containerPtr) != 0 {272+ parent := webkit.WidgetGetParent(containerPtr)
273+ if parent != 0 {
274+ log.Printf("[workspace] unparenting new pane container %#x from parent %#x before stack append", containerPtr, parent)
270 webkit.WidgetUnparent(containerPtr)275 webkit.WidgetUnparent(containerPtr)
276+ // Verify unparent succeeded
277+ if finalParent := webkit.WidgetGetParent(containerPtr); finalParent != 0 {
278+ log.Printf("[workspace] WARNING: container %#x still has parent %#x after unparent", containerPtr, finalParent)
279+ }
271 }280 }
272 return nil281 return nil
273 })282 })
M internal/app/browser/workspace_state_tombstone.go +7 -7
@@ -18,7 +18,7 @@ type StateTombstone struct {
1818 ID string
1919 Timestamp time.Time
2020 Operation string
21- TreeState *TreeSnapshot
21+ TreeState *TombstoneTreeSnapshot
2222 WidgetState *WidgetSnapshot
2323 FocusState *FocusSnapshot
2424 StackState *StackSnapshot
@@ -26,8 +26,8 @@ type StateTombstone struct {
2626 Size int
2727 }
2828
29-// TreeSnapshot captures the tree structure
30-type TreeSnapshot struct {
29+// TombstoneTreeSnapshot captures the tree structure for state tombstones
30+type TombstoneTreeSnapshot struct {
3131 RootID string
3232 MainPaneID string
3333 Nodes map[string]*NodeSnapshot
@@ -106,7 +106,7 @@ var gobRegisterOnce sync.Once
106106
107107 func registerTombstoneGobTypes() {
108108 gobRegisterOnce.Do(func() {
109- gob.Register(&TreeSnapshot{})
109+ gob.Register(&TombstoneTreeSnapshot{})
110110 gob.Register(&NodeSnapshot{})
111111 gob.Register(&WidgetSnapshot{})
112112 gob.Register(&FocusSnapshot{})
@@ -207,8 +207,8 @@ func (stm *StateTombstoneManager) CaptureState(operation string) (*StateTombston
207207 }
208208
209209 // captureTreeState captures the current tree structure
210-func (stm *StateTombstoneManager) captureTreeState() (*TreeSnapshot, error) {
211- snapshot := &TreeSnapshot{
210+func (stm *StateTombstoneManager) captureTreeState() (*TombstoneTreeSnapshot, error) {
211+ snapshot := &TombstoneTreeSnapshot{
212212 Nodes: make(map[string]*NodeSnapshot),
213213 ParentLinks: make(map[string]string),
214214 ChildLinks: make(map[string][2]string),
@@ -232,7 +232,7 @@ func (stm *StateTombstoneManager) captureTreeState() (*TreeSnapshot, error) {
232232 }
233233
234234 // captureNodeRecursive recursively captures node information
235-func (stm *StateTombstoneManager) captureNodeRecursive(node *paneNode, snapshot *TreeSnapshot) {
235+func (stm *StateTombstoneManager) captureNodeRecursive(node *paneNode, snapshot *TombstoneTreeSnapshot) {
236236 if node == nil {
237237 return
238238 }
@@ -18,7 +18,7 @@ type StateTombstone struct {
18 ID string18 ID string
19 Timestamp time.Time19 Timestamp time.Time
20 Operation string20 Operation string
21- TreeState *TreeSnapshot21+ TreeState *TombstoneTreeSnapshot
22 WidgetState *WidgetSnapshot22 WidgetState *WidgetSnapshot
23 FocusState *FocusSnapshot23 FocusState *FocusSnapshot
24 StackState *StackSnapshot24 StackState *StackSnapshot
@@ -26,8 +26,8 @@ type StateTombstone struct {
26 Size int26 Size int
27 }27 }
28 28
29-// TreeSnapshot captures the tree structure29+// TombstoneTreeSnapshot captures the tree structure for state tombstones
30-type TreeSnapshot struct {30+type TombstoneTreeSnapshot struct {
31 RootID string31 RootID string
32 MainPaneID string32 MainPaneID string
33 Nodes map[string]*NodeSnapshot33 Nodes map[string]*NodeSnapshot
@@ -106,7 +106,7 @@ var gobRegisterOnce sync.Once
106 106
107 func registerTombstoneGobTypes() {107 func registerTombstoneGobTypes() {
108 gobRegisterOnce.Do(func() {108 gobRegisterOnce.Do(func() {
109- gob.Register(&TreeSnapshot{})109+ gob.Register(&TombstoneTreeSnapshot{})
110 gob.Register(&NodeSnapshot{})110 gob.Register(&NodeSnapshot{})
111 gob.Register(&WidgetSnapshot{})111 gob.Register(&WidgetSnapshot{})
112 gob.Register(&FocusSnapshot{})112 gob.Register(&FocusSnapshot{})
@@ -207,8 +207,8 @@ func (stm *StateTombstoneManager) CaptureState(operation string) (*StateTombston
207 }207 }
208 208
209 // captureTreeState captures the current tree structure209 // captureTreeState captures the current tree structure
210-func (stm *StateTombstoneManager) captureTreeState() (*TreeSnapshot, error) {210+func (stm *StateTombstoneManager) captureTreeState() (*TombstoneTreeSnapshot, error) {
211- snapshot := &TreeSnapshot{211+ snapshot := &TombstoneTreeSnapshot{
212 Nodes: make(map[string]*NodeSnapshot),212 Nodes: make(map[string]*NodeSnapshot),
213 ParentLinks: make(map[string]string),213 ParentLinks: make(map[string]string),
214 ChildLinks: make(map[string][2]string),214 ChildLinks: make(map[string][2]string),
@@ -232,7 +232,7 @@ func (stm *StateTombstoneManager) captureTreeState() (*TreeSnapshot, error) {
232 }232 }
233 233
234 // captureNodeRecursive recursively captures node information234 // captureNodeRecursive recursively captures node information
235-func (stm *StateTombstoneManager) captureNodeRecursive(node *paneNode, snapshot *TreeSnapshot) {235+func (stm *StateTombstoneManager) captureNodeRecursive(node *paneNode, snapshot *TombstoneTreeSnapshot) {
236 if node == nil {236 if node == nil {
237 return237 return
238 }238 }
M internal/app/browser/workspace_tree_rebalancer.go +33 -11
@@ -622,15 +622,14 @@ func (tr *TreeRebalancer) executePromotion(node *paneNode, tx *WidgetTransaction
622622 log.Printf("[tree-rebalancer] unparenting widget from box (stack) %#x", currentParent)
623623 webkit.WidgetUnparent(ptr)
624624 } else {
625- // Parent is not a container - likely already attached to window
626- log.Printf("[tree-rebalancer] widget already attached to non-container parent %#x, skipping unparent", currentParent)
627- // Just ensure proper expansion and visibility
628- webkit.WidgetSetHExpand(ptr, true)
629- webkit.WidgetSetVExpand(ptr, true)
630- webkit.WidgetQueueAllocate(ptr)
631- webkit.WidgetShow(ptr)
632- log.Printf("[tree-rebalancer] promotion window attach successful: widget %#x already has parent %#x", ptr, currentParent)
633- return nil
625+ // Parent is likely a window or other non-container widget
626+ // For safety, check if it's a toplevel window by attempting unparent
627+ log.Printf("[tree-rebalancer] widget %#x has non-container parent %#x, checking if unparent needed", ptr, currentParent)
628+ if webkit.WidgetIsValid(currentParent) {
629+ // If parent is valid but not a container, try unparent for safety
630+ webkit.WidgetUnparent(ptr)
631+ log.Printf("[tree-rebalancer] unparented widget %#x from non-container parent %#x", ptr, currentParent)
632+ }
634633 }
635634 }
636635
@@ -687,16 +686,39 @@ func (tr *TreeRebalancer) executePromotion(node *paneNode, tx *WidgetTransaction
687686 if childPtr == 0 || !webkit.WidgetIsValid(childPtr) {
688687 return fmt.Errorf("promotion reparent: invalid child widget")
689688 }
690- if webkit.WidgetGetParent(childPtr) != 0 {
689+
690+ // Check if widget is already correctly parented (closePane.swapContainers already did the work)
691+ currentParent := webkit.WidgetGetParent(childPtr)
692+ if currentParent == parentPtr {
693+ log.Printf("[tree-rebalancer] widget %#x already correctly parented to %#x, skipping reparent", childPtr, parentPtr)
694+ // Just ensure properties are set correctly
695+ webkit.WidgetResetSizeRequest(childPtr)
696+ webkit.WidgetSetHExpand(childPtr, true)
697+ webkit.WidgetSetVExpand(childPtr, true)
698+ webkit.WidgetQueueAllocate(childPtr)
699+ webkit.WidgetQueueAllocate(parentPtr)
700+ return nil
701+ }
702+
703+ // Widget needs reparenting - unparent from wrong parent if needed
704+ if currentParent != 0 {
705+ log.Printf("[tree-rebalancer] unparenting widget %#x from incorrect parent %#x", childPtr, currentParent)
691706 webkit.WidgetUnparent(childPtr)
692707 }
693708
694- // Reattach promoted widget to the GtkPaned and request full allocation
709+ // Reattach promoted widget to the GtkPaned
695710 if parent.left == node {
696711 webkit.PanedSetStartChild(parentPtr, childPtr)
697712 } else {
698713 webkit.PanedSetEndChild(parentPtr, childPtr)
699714 }
715+
716+ // Verify attachment succeeded before setting properties
717+ finalParent := webkit.WidgetGetParent(childPtr)
718+ if finalParent != parentPtr {
719+ return fmt.Errorf("failed to attach widget %#x to parent %#x (current parent: %#x)", childPtr, parentPtr, finalParent)
720+ }
721+
700722 webkit.WidgetResetSizeRequest(childPtr)
701723 webkit.WidgetSetHExpand(childPtr, true)
702724 webkit.WidgetSetVExpand(childPtr, true)
@@ -622,15 +622,14 @@ func (tr *TreeRebalancer) executePromotion(node *paneNode, tx *WidgetTransaction
622 log.Printf("[tree-rebalancer] unparenting widget from box (stack) %#x", currentParent)622 log.Printf("[tree-rebalancer] unparenting widget from box (stack) %#x", currentParent)
623 webkit.WidgetUnparent(ptr)623 webkit.WidgetUnparent(ptr)
624 } else {624 } else {
625- // Parent is not a container - likely already attached to window625+ // Parent is likely a window or other non-container widget
626- log.Printf("[tree-rebalancer] widget already attached to non-container parent %#x, skipping unparent", currentParent)626+ // For safety, check if it's a toplevel window by attempting unparent
627- // Just ensure proper expansion and visibility627+ log.Printf("[tree-rebalancer] widget %#x has non-container parent %#x, checking if unparent needed", ptr, currentParent)
628- webkit.WidgetSetHExpand(ptr, true)628+ if webkit.WidgetIsValid(currentParent) {
629- webkit.WidgetSetVExpand(ptr, true)629+ // If parent is valid but not a container, try unparent for safety
630- webkit.WidgetQueueAllocate(ptr)630+ webkit.WidgetUnparent(ptr)
631- webkit.WidgetShow(ptr)631+ log.Printf("[tree-rebalancer] unparented widget %#x from non-container parent %#x", ptr, currentParent)
632- log.Printf("[tree-rebalancer] promotion window attach successful: widget %#x already has parent %#x", ptr, currentParent)632+ }
633- return nil
634 }633 }
635 }634 }
636 635
@@ -687,16 +686,39 @@ func (tr *TreeRebalancer) executePromotion(node *paneNode, tx *WidgetTransaction
687 if childPtr == 0 || !webkit.WidgetIsValid(childPtr) {686 if childPtr == 0 || !webkit.WidgetIsValid(childPtr) {
688 return fmt.Errorf("promotion reparent: invalid child widget")687 return fmt.Errorf("promotion reparent: invalid child widget")
689 }688 }
690- if webkit.WidgetGetParent(childPtr) != 0 {689+
690+ // Check if widget is already correctly parented (closePane.swapContainers already did the work)
691+ currentParent := webkit.WidgetGetParent(childPtr)
692+ if currentParent == parentPtr {
693+ log.Printf("[tree-rebalancer] widget %#x already correctly parented to %#x, skipping reparent", childPtr, parentPtr)
694+ // Just ensure properties are set correctly
695+ webkit.WidgetResetSizeRequest(childPtr)
696+ webkit.WidgetSetHExpand(childPtr, true)
697+ webkit.WidgetSetVExpand(childPtr, true)
698+ webkit.WidgetQueueAllocate(childPtr)
699+ webkit.WidgetQueueAllocate(parentPtr)
700+ return nil
701+ }
702+
703+ // Widget needs reparenting - unparent from wrong parent if needed
704+ if currentParent != 0 {
705+ log.Printf("[tree-rebalancer] unparenting widget %#x from incorrect parent %#x", childPtr, currentParent)
691 webkit.WidgetUnparent(childPtr)706 webkit.WidgetUnparent(childPtr)
692 }707 }
693 708
694- // Reattach promoted widget to the GtkPaned and request full allocation709+ // Reattach promoted widget to the GtkPaned
695 if parent.left == node {710 if parent.left == node {
696 webkit.PanedSetStartChild(parentPtr, childPtr)711 webkit.PanedSetStartChild(parentPtr, childPtr)
697 } else {712 } else {
698 webkit.PanedSetEndChild(parentPtr, childPtr)713 webkit.PanedSetEndChild(parentPtr, childPtr)
699 }714 }
715+
716+ // Verify attachment succeeded before setting properties
717+ finalParent := webkit.WidgetGetParent(childPtr)
718+ if finalParent != parentPtr {
719+ return fmt.Errorf("failed to attach widget %#x to parent %#x (current parent: %#x)", childPtr, parentPtr, finalParent)
720+ }
721+
700 webkit.WidgetResetSizeRequest(childPtr)722 webkit.WidgetResetSizeRequest(childPtr)
701 webkit.WidgetSetHExpand(childPtr, true)723 webkit.WidgetSetHExpand(childPtr, true)
702 webkit.WidgetSetVExpand(childPtr, true)724 webkit.WidgetSetVExpand(childPtr, true)
M internal/app/browser/workspace_utils.go +10 -37
@@ -2,7 +2,6 @@
22 package browser
33
44 import (
5- "errors"
65 "fmt"
76 "log"
87
@@ -55,36 +54,6 @@ func mapDirection(direction string) (webkit.Orientation, bool) {
5554
5655 // Widget management utilities
5756
58-// validateWidgetsForReparenting validates that all widgets needed for reparenting are valid
59-func (wm *WorkspaceManager) validateWidgetsForReparenting(sibling, parent, grand *paneNode) error {
60- // Validate sibling container (this is the most critical one)
61- if sibling.container == nil {
62- return errors.New("sibling container is nil")
63- }
64- if !sibling.container.IsValid() {
65- return fmt.Errorf("sibling container %s is not valid", sibling.container.String())
66- }
67-
68- // Validate parent container if present
69- if parent != nil && parent.container != nil && !parent.container.IsValid() {
70- return fmt.Errorf("parent container %s is not valid", parent.container.String())
71- }
72-
73- // Validate grandparent container if present
74- if grand != nil && grand.container != nil && !grand.container.IsValid() {
75- return fmt.Errorf("grandparent container %s is not valid", grand.container.String())
76- }
77-
78- return nil
79-}
80-
81-// safeWidgetOperation performs a widget operation with proper locking and validation
82-func (wm *WorkspaceManager) safeWidgetOperation(operation func() error) error {
83- wm.widgetMutex.Lock()
84- defer wm.widgetMutex.Unlock()
85- return operation()
86-}
87-
8857 // registerWidget registers a widget with the registry and returns a SafeWidget
8958 func (wm *WorkspaceManager) registerWidget(ptr uintptr, typeInfo string) *SafeWidget {
9059 if ptr == 0 {
@@ -109,6 +78,10 @@ func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, containerPtr u
10978 // Initialize other widget fields as nil (will be set when needed)
11079 node.titleBar = nil
11180 node.stackWrapper = nil
81+
82+ // Initialize enhanced close refactoring fields
83+ node.widgetValid = true
84+ node.cleanupGeneration = 0
11285 }
11386
11487 // Helper functions for safe widget operations
@@ -223,9 +196,9 @@ func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) {
223196 if ctx.borderContainer != nil {
224197 var applied bool
225198 if err := ctx.borderContainer.Execute(func(containerPtr uintptr) error {
226- // Safety check: verify container pointer is valid
227- if containerPtr == 0 {
228- return fmt.Errorf("invalid container pointer")
199+ // Safety check: verify container pointer is valid and widget still exists
200+ if containerPtr == 0 || !webkit.WidgetIsValid(containerPtr) {
201+ return fmt.Errorf("invalid container pointer or destroyed widget")
229202 }
230203 if webkit.WidgetHasCSSClass(containerPtr, activePaneClass) {
231204 return nil
@@ -252,9 +225,9 @@ func (wm *WorkspaceManager) removeActivePaneBorder(node *paneNode) {
252225 if ctx.borderContainer != nil {
253226 var removed bool
254227 if err := ctx.borderContainer.Execute(func(containerPtr uintptr) error {
255- // Safety check: verify container pointer is valid
256- if containerPtr == 0 {
257- return fmt.Errorf("invalid container pointer")
228+ // Safety check: verify container pointer is valid and widget still exists
229+ if containerPtr == 0 || !webkit.WidgetIsValid(containerPtr) {
230+ return fmt.Errorf("invalid container pointer or destroyed widget")
258231 }
259232 if !webkit.WidgetHasCSSClass(containerPtr, activePaneClass) {
260233 return nil
@@ -2,7 +2,6 @@
2 package browser2 package browser
3 3
4 import (4 import (
5- "errors"
6 "fmt"5 "fmt"
7 "log"6 "log"
8 7
@@ -55,36 +54,6 @@ func mapDirection(direction string) (webkit.Orientation, bool) {
55 54
56 // Widget management utilities55 // Widget management utilities
57 56
58-// validateWidgetsForReparenting validates that all widgets needed for reparenting are valid
59-func (wm *WorkspaceManager) validateWidgetsForReparenting(sibling, parent, grand *paneNode) error {
60- // Validate sibling container (this is the most critical one)
61- if sibling.container == nil {
62- return errors.New("sibling container is nil")
63- }
64- if !sibling.container.IsValid() {
65- return fmt.Errorf("sibling container %s is not valid", sibling.container.String())
66- }
67-
68- // Validate parent container if present
69- if parent != nil && parent.container != nil && !parent.container.IsValid() {
70- return fmt.Errorf("parent container %s is not valid", parent.container.String())
71- }
72-
73- // Validate grandparent container if present
74- if grand != nil && grand.container != nil && !grand.container.IsValid() {
75- return fmt.Errorf("grandparent container %s is not valid", grand.container.String())
76- }
77-
78- return nil
79-}
80-
81-// safeWidgetOperation performs a widget operation with proper locking and validation
82-func (wm *WorkspaceManager) safeWidgetOperation(operation func() error) error {
83- wm.widgetMutex.Lock()
84- defer wm.widgetMutex.Unlock()
85- return operation()
86-}
87-
88 // registerWidget registers a widget with the registry and returns a SafeWidget57 // registerWidget registers a widget with the registry and returns a SafeWidget
89 func (wm *WorkspaceManager) registerWidget(ptr uintptr, typeInfo string) *SafeWidget {58 func (wm *WorkspaceManager) registerWidget(ptr uintptr, typeInfo string) *SafeWidget {
90 if ptr == 0 {59 if ptr == 0 {
@@ -109,6 +78,10 @@ func (wm *WorkspaceManager) initializePaneWidgets(node *paneNode, containerPtr u
109 // Initialize other widget fields as nil (will be set when needed)78 // Initialize other widget fields as nil (will be set when needed)
110 node.titleBar = nil79 node.titleBar = nil
111 node.stackWrapper = nil80 node.stackWrapper = nil
81+
82+ // Initialize enhanced close refactoring fields
83+ node.widgetValid = true
84+ node.cleanupGeneration = 0
112 }85 }
113 86
114 // Helper functions for safe widget operations87 // Helper functions for safe widget operations
@@ -223,9 +196,9 @@ func (wm *WorkspaceManager) applyActivePaneBorder(node *paneNode) {
223 if ctx.borderContainer != nil {196 if ctx.borderContainer != nil {
224 var applied bool197 var applied bool
225 if err := ctx.borderContainer.Execute(func(containerPtr uintptr) error {198 if err := ctx.borderContainer.Execute(func(containerPtr uintptr) error {
226- // Safety check: verify container pointer is valid199+ // Safety check: verify container pointer is valid and widget still exists
227- if containerPtr == 0 {200+ if containerPtr == 0 || !webkit.WidgetIsValid(containerPtr) {
228- return fmt.Errorf("invalid container pointer")201+ return fmt.Errorf("invalid container pointer or destroyed widget")
229 }202 }
230 if webkit.WidgetHasCSSClass(containerPtr, activePaneClass) {203 if webkit.WidgetHasCSSClass(containerPtr, activePaneClass) {
231 return nil204 return nil
@@ -252,9 +225,9 @@ func (wm *WorkspaceManager) removeActivePaneBorder(node *paneNode) {
252 if ctx.borderContainer != nil {225 if ctx.borderContainer != nil {
253 var removed bool226 var removed bool
254 if err := ctx.borderContainer.Execute(func(containerPtr uintptr) error {227 if err := ctx.borderContainer.Execute(func(containerPtr uintptr) error {
255- // Safety check: verify container pointer is valid228+ // Safety check: verify container pointer is valid and widget still exists
256- if containerPtr == 0 {229+ if containerPtr == 0 || !webkit.WidgetIsValid(containerPtr) {
257- return fmt.Errorf("invalid container pointer")230+ return fmt.Errorf("invalid container pointer or destroyed widget")
258 }231 }
259 if !webkit.WidgetHasCSSClass(containerPtr, activePaneClass) {232 if !webkit.WidgetHasCSSClass(containerPtr, activePaneClass) {
260 return nil233 return nil

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