detached adb5e50

fix(workspace): address copilot PR review feedback - add validation, constants, and documentation improvements

- Extract popup dimension constants and focus throttle interval to named constants
- Add direction validation in SplitPane and FocusNeighbor with error messages
- Improve WidgetUnparentChecked documentation with detailed return semantics
- Add child widget validation in WidgetSetFocusChild
- Clarify WidgetWaitForDraw comment as no-op stub (no synchronization guarantees)
- Add deprecation note to TreeRebalancer indicating temporary disablement

Note: Deferred complex refactoring suggestions (closePane, scheduleIdleGuarded TOCTOU, OAuth URL parsing) for separate focused review to avoid introducing regressions.
commit
adb5e50da6c6eabafbe2c029c165a0bb0d802518
author
Brice · · 2025-10-05 15:30 UTC
parent
8ec5d1d
signature
unsigned
6 files · +43 -8
M internal/app/browser/workspace_navigation.go +6 -1
@@ -11,6 +11,11 @@ import (
1111 "github.com/bnema/dumber/pkg/webkit"
1212 )
1313
14+const (
15+ // focusThrottleInterval prevents rapid focus changes that could cause infinite loops
16+ focusThrottleInterval = 100 * time.Millisecond
17+)
18+
1419 // DispatchPaneFocusEvent sends a workspace focus event to a pane's webview
1520 func (wm *WorkspaceManager) DispatchPaneFocusEvent(node *paneNode, active bool) {
1621 if node == nil || node.pane == nil || node.pane.webView == nil {
@@ -69,7 +74,6 @@ func (wm *WorkspaceManager) focusByView(view *webkit.WebView) {
6974
7075 // Throttle focus changes to prevent infinite loops
7176 wm.focusThrottleMutex.Lock()
72- const focusThrottleInterval = 100 * time.Millisecond
7377 if time.Since(wm.lastFocusChange) < focusThrottleInterval {
7478 wm.focusThrottleMutex.Unlock()
7579 return
@@ -155,6 +159,7 @@ func (wm *WorkspaceManager) FocusNeighbor(direction string) bool {
155159 case "left", "right":
156160 return wm.focusAdjacent(strings.ToLower(direction))
157161 default:
162+ log.Printf("[workspace] invalid focus direction: %s (expected: up, down, left, right)", direction)
158163 return false
159164 }
160165 }
@@ -11,6 +11,11 @@ import (
11 "github.com/bnema/dumber/pkg/webkit"11 "github.com/bnema/dumber/pkg/webkit"
12 )12 )
13 13
14+const (
15+ // focusThrottleInterval prevents rapid focus changes that could cause infinite loops
16+ focusThrottleInterval = 100 * time.Millisecond
17+)
18+
14 // DispatchPaneFocusEvent sends a workspace focus event to a pane's webview19 // DispatchPaneFocusEvent sends a workspace focus event to a pane's webview
15 func (wm *WorkspaceManager) DispatchPaneFocusEvent(node *paneNode, active bool) {20 func (wm *WorkspaceManager) DispatchPaneFocusEvent(node *paneNode, active bool) {
16 if node == nil || node.pane == nil || node.pane.webView == nil {21 if node == nil || node.pane == nil || node.pane.webView == nil {
@@ -69,7 +74,6 @@ func (wm *WorkspaceManager) focusByView(view *webkit.WebView) {
69 74
70 // Throttle focus changes to prevent infinite loops75 // Throttle focus changes to prevent infinite loops
71 wm.focusThrottleMutex.Lock()76 wm.focusThrottleMutex.Lock()
72- const focusThrottleInterval = 100 * time.Millisecond
73 if time.Since(wm.lastFocusChange) < focusThrottleInterval {77 if time.Since(wm.lastFocusChange) < focusThrottleInterval {
74 wm.focusThrottleMutex.Unlock()78 wm.focusThrottleMutex.Unlock()
75 return79 return
@@ -155,6 +159,7 @@ func (wm *WorkspaceManager) FocusNeighbor(direction string) bool {
155 case "left", "right":159 case "left", "right":
156 return wm.focusAdjacent(strings.ToLower(direction))160 return wm.focusAdjacent(strings.ToLower(direction))
157 default:161 default:
162+ log.Printf("[workspace] invalid focus direction: %s (expected: up, down, left, right)", direction)
158 return false163 return false
159 }164 }
160 }165 }
M internal/app/browser/workspace_operations.go +11 -0
@@ -14,6 +14,17 @@ func (wm *WorkspaceManager) SplitPane(target *paneNode, direction string) (*pane
1414 return nil, fmt.Errorf("workspace manager is nil")
1515 }
1616
17+ // Validate direction parameter
18+ validDirections := map[string]bool{
19+ "left": true,
20+ "right": true,
21+ "up": true,
22+ "down": true,
23+ }
24+ if !validDirections[direction] {
25+ return nil, fmt.Errorf("invalid split direction '%s', expected one of: left, right, up, down", direction)
26+ }
27+
1728 log.Printf("[workspace] Starting split operation: target=%p direction=%s", target, direction)
1829
1930 // Step 1: Validate geometry constraints (only in DebugBasic+)
@@ -14,6 +14,17 @@ func (wm *WorkspaceManager) SplitPane(target *paneNode, direction string) (*pane
14 return nil, fmt.Errorf("workspace manager is nil")14 return nil, fmt.Errorf("workspace manager is nil")
15 }15 }
16 16
17+ // Validate direction parameter
18+ validDirections := map[string]bool{
19+ "left": true,
20+ "right": true,
21+ "up": true,
22+ "down": true,
23+ }
24+ if !validDirections[direction] {
25+ return nil, fmt.Errorf("invalid split direction '%s', expected one of: left, right, up, down", direction)
26+ }
27+
17 log.Printf("[workspace] Starting split operation: target=%p direction=%s", target, direction)28 log.Printf("[workspace] Starting split operation: target=%p direction=%s", target, direction)
18 29
19 // Step 1: Validate geometry constraints (only in DebugBasic+)30 // Step 1: Validate geometry constraints (only in DebugBasic+)
M internal/app/browser/workspace_popup.go +8 -2
@@ -11,6 +11,12 @@ import (
1111 "github.com/bnema/dumber/pkg/webkit"
1212 )
1313
14+const (
15+ // Default popup window dimensions - sized for OAuth flows
16+ defaultPopupMinWidth = 500 // Minimum width for OAuth popups
17+ defaultPopupMinHeight = 600 // Minimum height for OAuth popups
18+)
19+
1420 // HandlePopup handles popup window creation requests from WebViews
1521 func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *webkit.WebView {
1622 log.Printf("[workspace] HandlePopup called for URL: %s", url)
@@ -697,8 +703,8 @@ func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView
697703
698704 // applyPopupSizeConstraints applies minimum size constraints to prevent OAuth popup compression
699705 func (wm *WorkspaceManager) applyPopupSizeConstraints(view *webkit.WebView, width, height int) {
700- minWidth := 500 // Default minimum width
701- minHeight := 600 // Default minimum height
706+ minWidth := defaultPopupMinWidth
707+ minHeight := defaultPopupMinHeight
702708
703709 // Use provided dimensions if valid
704710 if width > 0 {
@@ -11,6 +11,12 @@ import (
11 "github.com/bnema/dumber/pkg/webkit"11 "github.com/bnema/dumber/pkg/webkit"
12 )12 )
13 13
14+const (
15+ // Default popup window dimensions - sized for OAuth flows
16+ defaultPopupMinWidth = 500 // Minimum width for OAuth popups
17+ defaultPopupMinHeight = 600 // Minimum height for OAuth popups
18+)
19+
14 // HandlePopup handles popup window creation requests from WebViews20 // HandlePopup handles popup window creation requests from WebViews
15 func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *webkit.WebView {21 func (wm *WorkspaceManager) HandlePopup(source *webkit.WebView, url string) *webkit.WebView {
16 log.Printf("[workspace] HandlePopup called for URL: %s", url)22 log.Printf("[workspace] HandlePopup called for URL: %s", url)
@@ -697,8 +703,8 @@ func (wm *WorkspaceManager) configureRelatedPopup(sourceNode *paneNode, webView
697 703
698 // applyPopupSizeConstraints applies minimum size constraints to prevent OAuth popup compression704 // applyPopupSizeConstraints applies minimum size constraints to prevent OAuth popup compression
699 func (wm *WorkspaceManager) applyPopupSizeConstraints(view *webkit.WebView, width, height int) {705 func (wm *WorkspaceManager) applyPopupSizeConstraints(view *webkit.WebView, width, height int) {
700- minWidth := 500 // Default minimum width706+ minWidth := defaultPopupMinWidth
701- minHeight := 600 // Default minimum height707+ minHeight := defaultPopupMinHeight
702 708
703 // Use provided dimensions if valid709 // Use provided dimensions if valid
704 if width > 0 {710 if width > 0 {
M internal/app/browser/workspace_tree_rebalancer.go +2 -0
@@ -32,6 +32,8 @@ const (
3232 type TreeMetrics struct{}
3333
3434 // NewTreeRebalancer returns a disabled stub implementation.
35+// NOTE: Tree rebalancing logic has been temporarily disabled in favor of manual geometry management.
36+// Future work may restore automatic rebalancing or replace it with an alternative approach.
3537 func NewTreeRebalancer(_ *WorkspaceManager, _ *TreeValidator) *TreeRebalancer {
3638 return &TreeRebalancer{}
3739 }
@@ -32,6 +32,8 @@ const (
32 type TreeMetrics struct{}32 type TreeMetrics struct{}
33 33
34 // NewTreeRebalancer returns a disabled stub implementation.34 // NewTreeRebalancer returns a disabled stub implementation.
35+// NOTE: Tree rebalancing logic has been temporarily disabled in favor of manual geometry management.
36+// Future work may restore automatic rebalancing or replace it with an alternative approach.
35 func NewTreeRebalancer(_ *WorkspaceManager, _ *TreeValidator) *TreeRebalancer {37 func NewTreeRebalancer(_ *WorkspaceManager, _ *TreeValidator) *TreeRebalancer {
36 return &TreeRebalancer{}38 return &TreeRebalancer{}
37 }39 }
M pkg/webkit/workspace_widgets_cgo.go +12 -1
@@ -609,7 +609,14 @@ func WidgetUnparent(widget uintptr) {
609609 }
610610
611611 // WidgetUnparentChecked safely detaches a widget from its parent if still valid.
612-// Returns true when the widget was unparented or already detached.
612+//
613+// Return semantics:
614+// - Returns false if the widget pointer is zero or not a valid GTK widget (invalid input).
615+// - Returns true if the widget has no parent (already detached).
616+// - Returns true if the widget was successfully unparented (parent changed or is now nil).
617+// - Returns false if the widget is still valid and the parent did not change (unparent failed).
618+//
619+// Callers can use the return value to distinguish between invalid input, already-detached widgets, and successful unparenting.
613620 func WidgetUnparentChecked(widget uintptr) bool {
614621 if widget == 0 {
615622 log.Printf("[workspace] WidgetUnparentChecked: widget=0")
@@ -728,6 +735,10 @@ func WidgetSetFocusChild(widget uintptr, child uintptr) {
728735 }
729736 var childPtr *C.GtkWidget
730737 if child != 0 {
738+ if !widgetIsValid(child) {
739+ log.Printf("[workspace] WidgetSetFocusChild skipped invalid child=%#x", child)
740+ return
741+ }
731742 childPtr = (*C.GtkWidget)(unsafe.Pointer(child))
732743 }
733744 C.gtk_widget_set_focus_child((*C.GtkWidget)(unsafe.Pointer(widget)), childPtr)
@@ -609,7 +609,14 @@ func WidgetUnparent(widget uintptr) {
609 }609 }
610 610
611 // WidgetUnparentChecked safely detaches a widget from its parent if still valid.611 // WidgetUnparentChecked safely detaches a widget from its parent if still valid.
612-// Returns true when the widget was unparented or already detached.612+//
613+// Return semantics:
614+// - Returns false if the widget pointer is zero or not a valid GTK widget (invalid input).
615+// - Returns true if the widget has no parent (already detached).
616+// - Returns true if the widget was successfully unparented (parent changed or is now nil).
617+// - Returns false if the widget is still valid and the parent did not change (unparent failed).
618+//
619+// Callers can use the return value to distinguish between invalid input, already-detached widgets, and successful unparenting.
613 func WidgetUnparentChecked(widget uintptr) bool {620 func WidgetUnparentChecked(widget uintptr) bool {
614 if widget == 0 {621 if widget == 0 {
615 log.Printf("[workspace] WidgetUnparentChecked: widget=0")622 log.Printf("[workspace] WidgetUnparentChecked: widget=0")
@@ -728,6 +735,10 @@ func WidgetSetFocusChild(widget uintptr, child uintptr) {
728 }735 }
729 var childPtr *C.GtkWidget736 var childPtr *C.GtkWidget
730 if child != 0 {737 if child != 0 {
738+ if !widgetIsValid(child) {
739+ log.Printf("[workspace] WidgetSetFocusChild skipped invalid child=%#x", child)
740+ return
741+ }
731 childPtr = (*C.GtkWidget)(unsafe.Pointer(child))742 childPtr = (*C.GtkWidget)(unsafe.Pointer(child))
732 }743 }
733 C.gtk_widget_set_focus_child((*C.GtkWidget)(unsafe.Pointer(widget)), childPtr)744 C.gtk_widget_set_focus_child((*C.GtkWidget)(unsafe.Pointer(widget)), childPtr)
M pkg/webkit/workspace_widgets_stub.go +4 -4
@@ -401,8 +401,10 @@ func WidgetGetBounds(widget uintptr) (WidgetBounds, bool) {
401401 return stub.bounds, true
402402 }
403403
404-// WidgetWaitForDraw ensures widget operations are complete (GTK test pattern)
405-// This replaces the problematic IdleAdd pattern with proper widget synchronization
404+// WidgetWaitForDraw is a no-op validation stub in this build.
405+// It does NOT provide any synchronization or guarantee that widget draw operations are complete.
406+// In the stub, it only validates the widget state and does not wait for any operations.
407+// In real GTK builds, this would wait for all pending draw operations to complete.
406408 func WidgetWaitForDraw(widget uintptr) {
407409 gtkTestInitOnce.Do(func() {})
408410 widgetMu.Lock()
@@ -415,8 +417,6 @@ func WidgetWaitForDraw(widget uintptr) {
415417 panic("GTK-CRITICAL simulation: gtk_test_widget_wait_for_draw: widget is invalid")
416418 }
417419 }
418- // In real GTK, this would wait for all pending draw operations to complete
419- // In our stub, we just validate the widget state
420420 }
421421
422422 // IdleAdd simulates GTK's idle callback system - DEPRECATED, use WidgetWaitForDraw instead
@@ -401,8 +401,10 @@ func WidgetGetBounds(widget uintptr) (WidgetBounds, bool) {
401 return stub.bounds, true401 return stub.bounds, true
402 }402 }
403 403
404-// WidgetWaitForDraw ensures widget operations are complete (GTK test pattern)404+// WidgetWaitForDraw is a no-op validation stub in this build.
405-// This replaces the problematic IdleAdd pattern with proper widget synchronization405+// It does NOT provide any synchronization or guarantee that widget draw operations are complete.
406+// In the stub, it only validates the widget state and does not wait for any operations.
407+// In real GTK builds, this would wait for all pending draw operations to complete.
406 func WidgetWaitForDraw(widget uintptr) {408 func WidgetWaitForDraw(widget uintptr) {
407 gtkTestInitOnce.Do(func() {})409 gtkTestInitOnce.Do(func() {})
408 widgetMu.Lock()410 widgetMu.Lock()
@@ -415,8 +417,6 @@ func WidgetWaitForDraw(widget uintptr) {
415 panic("GTK-CRITICAL simulation: gtk_test_widget_wait_for_draw: widget is invalid")417 panic("GTK-CRITICAL simulation: gtk_test_widget_wait_for_draw: widget is invalid")
416 }418 }
417 }419 }
418- // In real GTK, this would wait for all pending draw operations to complete
419- // In our stub, we just validate the widget state
420 }420 }
421 421
422 // IdleAdd simulates GTK's idle callback system - DEPRECATED, use WidgetWaitForDraw instead422 // IdleAdd simulates GTK's idle callback system - DEPRECATED, use WidgetWaitForDraw instead

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