detached 91bff85

feat(workspace): implement debug logging and tree diagnostics for pane close operations

commit
91bff85453c4a21dd39445cb31456a522e274393
author
Brice · · 2025-09-30 05:15 UTC
parent
e182a2c
signature
unsigned
3 files · +202 -0
M internal/app/browser/workspace_bulletproof_operations.go +11 -0
@@ -125,6 +125,8 @@ func (wm *WorkspaceManager) BulletproofClosePane(node *paneNode) error {
125125 }
126126
127127 log.Printf("[bulletproof] Starting bulletproof close operation: node=%p", node)
128+ wm.paneCloseLogf("start bulletproof close node=%p", node)
129+ wm.dumpTreeState("before_close")
128130
129131 // Step 1: Capture state tombstone for rollback
130132 tombstone, err := wm.stateTombstoneManager.CaptureState("close")
@@ -147,8 +149,11 @@ func (wm *WorkspaceManager) BulletproofClosePane(node *paneNode) error {
147149 if webkit.IsMainThread() {
148150 log.Printf("[bulletproof] Already on main thread, executing close directly")
149151
152+ wm.paneCloseLogf("invoking closePane node=%p", node)
150153 promoted, err := wm.closePane(node)
151154 if err != nil {
155+ wm.paneCloseLogf("closePane failed node=%p err=%v", node, err)
156+ wm.dumpTreeState("after_close_error")
152157 if tombstone != nil {
153158 if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
154159 log.Printf("[bulletproof] Rollback failed after close failure: %v", rollbackErr)
@@ -160,6 +165,8 @@ func (wm *WorkspaceManager) BulletproofClosePane(node *paneNode) error {
160165 if err := wm.treeValidator.ValidateTree(wm.root, "after_close"); err != nil {
161166 log.Printf("[bulletproof] Tree validation failed after close: %v", err)
162167 }
168+ wm.paneCloseLogf("closePane succeeded node=%p promoted=%p root=%p", node, promoted, wm.root)
169+ wm.dumpTreeState("after_close_success")
163170
164171 if wm.treeRebalancer != nil {
165172 if err := wm.treeRebalancer.RebalanceAfterClose(node, promoted); err != nil {
@@ -195,6 +202,8 @@ func (wm *WorkspaceManager) BulletproofClosePane(node *paneNode) error {
195202 select {
196203 case result := <-resultChan:
197204 if !result.Success {
205+ wm.paneCloseLogf("async close failed node=%p err=%v", node, result.Error)
206+ wm.dumpTreeState("after_close_error")
198207 if tombstone != nil {
199208 if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
200209 log.Printf("[bulletproof] Rollback failed after close failure: %v", rollbackErr)
@@ -206,6 +215,8 @@ func (wm *WorkspaceManager) BulletproofClosePane(node *paneNode) error {
206215 if err := wm.treeValidator.ValidateTree(wm.root, "after_close"); err != nil {
207216 log.Printf("[bulletproof] Tree validation failed after close: %v", err)
208217 }
218+ wm.paneCloseLogf("async close succeeded node=%p promoted=%p root=%p", node, result.NewNode, wm.root)
219+ wm.dumpTreeState("after_close_success")
209220
210221 if wm.treeRebalancer != nil {
211222 if err := wm.treeRebalancer.RebalanceAfterClose(node, result.NewNode); err != nil {
@@ -125,6 +125,8 @@ func (wm *WorkspaceManager) BulletproofClosePane(node *paneNode) error {
125 }125 }
126 126
127 log.Printf("[bulletproof] Starting bulletproof close operation: node=%p", node)127 log.Printf("[bulletproof] Starting bulletproof close operation: node=%p", node)
128+ wm.paneCloseLogf("start bulletproof close node=%p", node)
129+ wm.dumpTreeState("before_close")
128 130
129 // Step 1: Capture state tombstone for rollback131 // Step 1: Capture state tombstone for rollback
130 tombstone, err := wm.stateTombstoneManager.CaptureState("close")132 tombstone, err := wm.stateTombstoneManager.CaptureState("close")
@@ -147,8 +149,11 @@ func (wm *WorkspaceManager) BulletproofClosePane(node *paneNode) error {
147 if webkit.IsMainThread() {149 if webkit.IsMainThread() {
148 log.Printf("[bulletproof] Already on main thread, executing close directly")150 log.Printf("[bulletproof] Already on main thread, executing close directly")
149 151
152+ wm.paneCloseLogf("invoking closePane node=%p", node)
150 promoted, err := wm.closePane(node)153 promoted, err := wm.closePane(node)
151 if err != nil {154 if err != nil {
155+ wm.paneCloseLogf("closePane failed node=%p err=%v", node, err)
156+ wm.dumpTreeState("after_close_error")
152 if tombstone != nil {157 if tombstone != nil {
153 if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {158 if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
154 log.Printf("[bulletproof] Rollback failed after close failure: %v", rollbackErr)159 log.Printf("[bulletproof] Rollback failed after close failure: %v", rollbackErr)
@@ -160,6 +165,8 @@ func (wm *WorkspaceManager) BulletproofClosePane(node *paneNode) error {
160 if err := wm.treeValidator.ValidateTree(wm.root, "after_close"); err != nil {165 if err := wm.treeValidator.ValidateTree(wm.root, "after_close"); err != nil {
161 log.Printf("[bulletproof] Tree validation failed after close: %v", err)166 log.Printf("[bulletproof] Tree validation failed after close: %v", err)
162 }167 }
168+ wm.paneCloseLogf("closePane succeeded node=%p promoted=%p root=%p", node, promoted, wm.root)
169+ wm.dumpTreeState("after_close_success")
163 170
164 if wm.treeRebalancer != nil {171 if wm.treeRebalancer != nil {
165 if err := wm.treeRebalancer.RebalanceAfterClose(node, promoted); err != nil {172 if err := wm.treeRebalancer.RebalanceAfterClose(node, promoted); err != nil {
@@ -195,6 +202,8 @@ func (wm *WorkspaceManager) BulletproofClosePane(node *paneNode) error {
195 select {202 select {
196 case result := <-resultChan:203 case result := <-resultChan:
197 if !result.Success {204 if !result.Success {
205+ wm.paneCloseLogf("async close failed node=%p err=%v", node, result.Error)
206+ wm.dumpTreeState("after_close_error")
198 if tombstone != nil {207 if tombstone != nil {
199 if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {208 if rollbackErr := wm.stateTombstoneManager.RestoreState(tombstone.ID); rollbackErr != nil {
200 log.Printf("[bulletproof] Rollback failed after close failure: %v", rollbackErr)209 log.Printf("[bulletproof] Rollback failed after close failure: %v", rollbackErr)
@@ -206,6 +215,8 @@ func (wm *WorkspaceManager) BulletproofClosePane(node *paneNode) error {
206 if err := wm.treeValidator.ValidateTree(wm.root, "after_close"); err != nil {215 if err := wm.treeValidator.ValidateTree(wm.root, "after_close"); err != nil {
207 log.Printf("[bulletproof] Tree validation failed after close: %v", err)216 log.Printf("[bulletproof] Tree validation failed after close: %v", err)
208 }217 }
218+ wm.paneCloseLogf("async close succeeded node=%p promoted=%p root=%p", node, result.NewNode, wm.root)
219+ wm.dumpTreeState("after_close_success")
209 220
210 if wm.treeRebalancer != nil {221 if wm.treeRebalancer != nil {
211 if err := wm.treeRebalancer.RebalanceAfterClose(node, result.NewNode); err != nil {222 if err := wm.treeRebalancer.RebalanceAfterClose(node, result.NewNode); err != nil {
A internal/app/browser/workspace_debug.go +36 -0
@@ -0,0 +1,36 @@
1+package browser
2+
3+import "log"
4+
5+func (wm *WorkspaceManager) paneCloseLogf(format string, args ...interface{}) {
6+ if wm == nil || !wm.debugPaneClose {
7+ return
8+ }
9+ log.Printf("[pane-close] "+format, args...)
10+}
11+
12+func (wm *WorkspaceManager) dumpTreeState(label string) {
13+ if wm == nil || wm.diagnostics == nil {
14+ return
15+ }
16+ wm.diagnostics.Capture(label, wm.root)
17+}
18+
19+func (wm *WorkspaceManager) DiagnosticsSnapshots() []TreeSnapshot {
20+ if wm == nil || wm.diagnostics == nil {
21+ return nil
22+ }
23+ return wm.diagnostics.Snapshots()
24+}
25+
26+func (wm *WorkspaceManager) SetPaneCloseDebug(enabled bool) {
27+ if wm == nil {
28+ return
29+ }
30+ wm.debugPaneClose = enabled
31+ if wm.diagnostics == nil {
32+ wm.diagnostics = NewWorkspaceDiagnostics(enabled)
33+ return
34+ }
35+ wm.diagnostics.SetEnabled(enabled)
36+}
@@ -0,0 +1,36 @@
1+package browser
2+
3+import "log"
4+
5+func (wm *WorkspaceManager) paneCloseLogf(format string, args ...interface{}) {
6+ if wm == nil || !wm.debugPaneClose {
7+ return
8+ }
9+ log.Printf("[pane-close] "+format, args...)
10+}
11+
12+func (wm *WorkspaceManager) dumpTreeState(label string) {
13+ if wm == nil || wm.diagnostics == nil {
14+ return
15+ }
16+ wm.diagnostics.Capture(label, wm.root)
17+}
18+
19+func (wm *WorkspaceManager) DiagnosticsSnapshots() []TreeSnapshot {
20+ if wm == nil || wm.diagnostics == nil {
21+ return nil
22+ }
23+ return wm.diagnostics.Snapshots()
24+}
25+
26+func (wm *WorkspaceManager) SetPaneCloseDebug(enabled bool) {
27+ if wm == nil {
28+ return
29+ }
30+ wm.debugPaneClose = enabled
31+ if wm.diagnostics == nil {
32+ wm.diagnostics = NewWorkspaceDiagnostics(enabled)
33+ return
34+ }
35+ wm.diagnostics.SetEnabled(enabled)
36+}
A internal/app/browser/workspace_diagnostics.go +155 -0
@@ -0,0 +1,155 @@
1+package browser
2+
3+import (
4+ "fmt"
5+ "log"
6+ "strings"
7+ "sync"
8+)
9+
10+type TreeSnapshot struct {
11+ Label string
12+ Lines []string
13+}
14+
15+type WorkspaceDiagnostics struct {
16+ mu sync.Mutex
17+ enabled bool
18+ snapshots []TreeSnapshot
19+}
20+
21+func NewWorkspaceDiagnostics(enabled bool) *WorkspaceDiagnostics {
22+ return &WorkspaceDiagnostics{enabled: enabled}
23+}
24+
25+func (d *WorkspaceDiagnostics) Enabled() bool {
26+ if d == nil {
27+ return false
28+ }
29+ d.mu.Lock()
30+ defer d.mu.Unlock()
31+ return d.enabled
32+}
33+
34+func (d *WorkspaceDiagnostics) SetEnabled(enabled bool) {
35+ if d == nil {
36+ return
37+ }
38+ d.mu.Lock()
39+ d.enabled = enabled
40+ d.mu.Unlock()
41+}
42+
43+func (d *WorkspaceDiagnostics) Capture(label string, root *paneNode) TreeSnapshot {
44+ if d == nil {
45+ return TreeSnapshot{}
46+ }
47+
48+ d.mu.Lock()
49+ enabled := d.enabled
50+ d.mu.Unlock()
51+ if !enabled {
52+ return TreeSnapshot{}
53+ }
54+
55+ snapshot := TreeSnapshot{Label: label}
56+ visited := make(map[*paneNode]bool)
57+ buildTreeSnapshot(root, 0, "", visited, &snapshot.Lines)
58+ if len(snapshot.Lines) == 0 {
59+ snapshot.Lines = append(snapshot.Lines, "<empty workspace>")
60+ }
61+
62+ d.mu.Lock()
63+ d.snapshots = append(d.snapshots, snapshot)
64+ d.mu.Unlock()
65+
66+ log.Printf("[pane-close] snapshot %s\n%s", snapshot.Label, strings.Join(snapshot.Lines, "\n"))
67+ return snapshot
68+}
69+
70+func (d *WorkspaceDiagnostics) Snapshots() []TreeSnapshot {
71+ if d == nil {
72+ return nil
73+ }
74+ d.mu.Lock()
75+ defer d.mu.Unlock()
76+
77+ out := make([]TreeSnapshot, len(d.snapshots))
78+ copy(out, d.snapshots)
79+ return out
80+}
81+
82+func buildTreeSnapshot(node *paneNode, depth int, prefix string, visited map[*paneNode]bool, lines *[]string) {
83+ indent := strings.Repeat(" ", depth)
84+ if node == nil {
85+ *lines = append(*lines, fmt.Sprintf("%s%s<nil>", indent, prefix))
86+ return
87+ }
88+
89+ if depth > 64 {
90+ *lines = append(*lines, fmt.Sprintf("%s%s<max depth reached>", indent, prefix))
91+ return
92+ }
93+
94+ if visited[node] {
95+ *lines = append(*lines, fmt.Sprintf("%s%s<cycle node=%p>", indent, prefix, node))
96+ return
97+ }
98+
99+ visited[node] = true
100+ *lines = append(*lines, fmt.Sprintf("%s%s%s", indent, prefix, describePaneNode(node)))
101+
102+ if node.isStacked {
103+ for idx, child := range node.stackedPanes {
104+ childPrefix := fmt.Sprintf("[%d] ", idx)
105+ buildTreeSnapshot(child, depth+1, childPrefix, visited, lines)
106+ }
107+ return
108+ }
109+
110+ if node.left != nil || node.right != nil {
111+ buildTreeSnapshot(node.left, depth+1, "L: ", visited, lines)
112+ buildTreeSnapshot(node.right, depth+1, "R: ", visited, lines)
113+ }
114+}
115+
116+func describePaneNode(node *paneNode) string {
117+ if node == nil {
118+ return "<nil>"
119+ }
120+
121+ parts := []string{fmt.Sprintf("node=%p", node)}
122+
123+ if node.isLeaf {
124+ parts = append(parts, "leaf")
125+ if node.pane != nil && node.pane.ID() != "" {
126+ parts = append(parts, fmt.Sprintf("pane=%s", node.pane.ID()))
127+ }
128+ if node.isPopup {
129+ parts = append(parts, "popup")
130+ }
131+ } else if node.isStacked {
132+ parts = append(parts, fmt.Sprintf("stack size=%d active=%d", len(node.stackedPanes), node.activeStackIndex))
133+ } else {
134+ parts = append(parts, fmt.Sprintf("split orientation=%d", node.orientation))
135+ }
136+
137+ if node.container != nil {
138+ parts = append(parts, fmt.Sprintf("widget=%#x", node.container.Ptr()))
139+ }
140+
141+ if node.parent != nil {
142+ parts = append(parts, fmt.Sprintf("parent=%p", node.parent))
143+ }
144+
145+ if !node.isLeaf && !node.isStacked {
146+ if node.left != nil {
147+ parts = append(parts, fmt.Sprintf("left=%p", node.left))
148+ }
149+ if node.right != nil {
150+ parts = append(parts, fmt.Sprintf("right=%p", node.right))
151+ }
152+ }
153+
154+ return strings.Join(parts, " ")
155+}
@@ -0,0 +1,155 @@
1+package browser
2+
3+import (
4+ "fmt"
5+ "log"
6+ "strings"
7+ "sync"
8+)
9+
10+type TreeSnapshot struct {
11+ Label string
12+ Lines []string
13+}
14+
15+type WorkspaceDiagnostics struct {
16+ mu sync.Mutex
17+ enabled bool
18+ snapshots []TreeSnapshot
19+}
20+
21+func NewWorkspaceDiagnostics(enabled bool) *WorkspaceDiagnostics {
22+ return &WorkspaceDiagnostics{enabled: enabled}
23+}
24+
25+func (d *WorkspaceDiagnostics) Enabled() bool {
26+ if d == nil {
27+ return false
28+ }
29+ d.mu.Lock()
30+ defer d.mu.Unlock()
31+ return d.enabled
32+}
33+
34+func (d *WorkspaceDiagnostics) SetEnabled(enabled bool) {
35+ if d == nil {
36+ return
37+ }
38+ d.mu.Lock()
39+ d.enabled = enabled
40+ d.mu.Unlock()
41+}
42+
43+func (d *WorkspaceDiagnostics) Capture(label string, root *paneNode) TreeSnapshot {
44+ if d == nil {
45+ return TreeSnapshot{}
46+ }
47+
48+ d.mu.Lock()
49+ enabled := d.enabled
50+ d.mu.Unlock()
51+ if !enabled {
52+ return TreeSnapshot{}
53+ }
54+
55+ snapshot := TreeSnapshot{Label: label}
56+ visited := make(map[*paneNode]bool)
57+ buildTreeSnapshot(root, 0, "", visited, &snapshot.Lines)
58+ if len(snapshot.Lines) == 0 {
59+ snapshot.Lines = append(snapshot.Lines, "<empty workspace>")
60+ }
61+
62+ d.mu.Lock()
63+ d.snapshots = append(d.snapshots, snapshot)
64+ d.mu.Unlock()
65+
66+ log.Printf("[pane-close] snapshot %s\n%s", snapshot.Label, strings.Join(snapshot.Lines, "\n"))
67+ return snapshot
68+}
69+
70+func (d *WorkspaceDiagnostics) Snapshots() []TreeSnapshot {
71+ if d == nil {
72+ return nil
73+ }
74+ d.mu.Lock()
75+ defer d.mu.Unlock()
76+
77+ out := make([]TreeSnapshot, len(d.snapshots))
78+ copy(out, d.snapshots)
79+ return out
80+}
81+
82+func buildTreeSnapshot(node *paneNode, depth int, prefix string, visited map[*paneNode]bool, lines *[]string) {
83+ indent := strings.Repeat(" ", depth)
84+ if node == nil {
85+ *lines = append(*lines, fmt.Sprintf("%s%s<nil>", indent, prefix))
86+ return
87+ }
88+
89+ if depth > 64 {
90+ *lines = append(*lines, fmt.Sprintf("%s%s<max depth reached>", indent, prefix))
91+ return
92+ }
93+
94+ if visited[node] {
95+ *lines = append(*lines, fmt.Sprintf("%s%s<cycle node=%p>", indent, prefix, node))
96+ return
97+ }
98+
99+ visited[node] = true
100+ *lines = append(*lines, fmt.Sprintf("%s%s%s", indent, prefix, describePaneNode(node)))
101+
102+ if node.isStacked {
103+ for idx, child := range node.stackedPanes {
104+ childPrefix := fmt.Sprintf("[%d] ", idx)
105+ buildTreeSnapshot(child, depth+1, childPrefix, visited, lines)
106+ }
107+ return
108+ }
109+
110+ if node.left != nil || node.right != nil {
111+ buildTreeSnapshot(node.left, depth+1, "L: ", visited, lines)
112+ buildTreeSnapshot(node.right, depth+1, "R: ", visited, lines)
113+ }
114+}
115+
116+func describePaneNode(node *paneNode) string {
117+ if node == nil {
118+ return "<nil>"
119+ }
120+
121+ parts := []string{fmt.Sprintf("node=%p", node)}
122+
123+ if node.isLeaf {
124+ parts = append(parts, "leaf")
125+ if node.pane != nil && node.pane.ID() != "" {
126+ parts = append(parts, fmt.Sprintf("pane=%s", node.pane.ID()))
127+ }
128+ if node.isPopup {
129+ parts = append(parts, "popup")
130+ }
131+ } else if node.isStacked {
132+ parts = append(parts, fmt.Sprintf("stack size=%d active=%d", len(node.stackedPanes), node.activeStackIndex))
133+ } else {
134+ parts = append(parts, fmt.Sprintf("split orientation=%d", node.orientation))
135+ }
136+
137+ if node.container != nil {
138+ parts = append(parts, fmt.Sprintf("widget=%#x", node.container.Ptr()))
139+ }
140+
141+ if node.parent != nil {
142+ parts = append(parts, fmt.Sprintf("parent=%p", node.parent))
143+ }
144+
145+ if !node.isLeaf && !node.isStacked {
146+ if node.left != nil {
147+ parts = append(parts, fmt.Sprintf("left=%p", node.left))
148+ }
149+ if node.right != nil {
150+ parts = append(parts, fmt.Sprintf("right=%p", node.right))
151+ }
152+ }
153+
154+ return strings.Join(parts, " ")
155+}

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