detached d148d83

fix: resolve stacked panes focus and event duplication issues

This commit addresses several critical issues with stacked panes:

**1. Fix duplicate workspace events**
- Remove redundant emitWorkspaceEvent calls in JavaScript workspace controller
- Keep only bridge() calls which are processed by the backend
- Eliminates repetitive "Forwarding workspace event" log messages

**2. Fix focus synchronization between Go and JavaScript**
- Update notifyJavaScript to inform both old and new panes of focus changes
- Send {active: true} to new pane and {active: false} to old pane
- Ensures omnibox and shortcuts target the correct active pane

**3. Fix app.activePane synchronization**
- Add app.activePane sync in FocusManager.SetActivePane
- Ensures window shortcuts use the same active pane as focus manager
- Resolves omnibox opening in wrong pane after navigation

**4. Fix focus order during pane transitions**
- Call SetActivePane before updating currentlyFocused in both:
  - finalizeStackCreation (stacked pane creation)
  - NavigateStack (Alt+Up/Down navigation)
- Prevents "already active" early returns that skip synchronization

**5. Improve stacked pane visibility logic**
- Process all panes in single loop to prevent flickering
- Add explicit logging for active pane title bar hiding
- Enforce absolute rule: active pane title bar is never visible

**Result**:
- Omnibox now opens in the correct active pane after navigation
- Focus changes are properly synchronized between Go and JavaScript
- Title bar visibility is consistent (only inactive panes show title bars)
- Single workspace event per action (no more duplicates)
commit
d148d83c217afa19a642337e7baaab6a04f3c1d7
author
bamen · · 2025-09-26 05:55 UTC
parent
fcf18d5
signature
unsigned
3 files · +43 -36
M gui/src/injected/modules/workspace.ts +0 -4
@@ -255,7 +255,6 @@ class WorkspaceController implements WorkspaceRuntime {
255255 clearTimeout(this.paneModeTimer);
256256 this.paneModeTimer = null;
257257 }
258- this.emitWorkspaceEvent("pane-mode-exited", { action: reason });
259258 this.bridge({ event: "pane-mode-exited", reason });
260259 }
261260
@@ -341,9 +340,6 @@ class WorkspaceController implements WorkspaceRuntime {
341340 this.exitPaneMode("split-down");
342341 break;
343342 case "stack-pane":
344- this.emitWorkspaceEvent("pane-stack", {
345- config: this.config,
346- });
347343 this.bridge({ event: "pane-stack", action: "stack-pane" });
348344 this.showToast("Stacked pane");
349345 this.exitPaneMode("stack-pane");
@@ -255,7 +255,6 @@ class WorkspaceController implements WorkspaceRuntime {
255 clearTimeout(this.paneModeTimer);255 clearTimeout(this.paneModeTimer);
256 this.paneModeTimer = null;256 this.paneModeTimer = null;
257 }257 }
258- this.emitWorkspaceEvent("pane-mode-exited", { action: reason });
259 this.bridge({ event: "pane-mode-exited", reason });258 this.bridge({ event: "pane-mode-exited", reason });
260 }259 }
261 260
@@ -341,9 +340,6 @@ class WorkspaceController implements WorkspaceRuntime {
341 this.exitPaneMode("split-down");340 this.exitPaneMode("split-down");
342 break;341 break;
343 case "stack-pane":342 case "stack-pane":
344- this.emitWorkspaceEvent("pane-stack", {
345- config: this.config,
346- });
347 this.bridge({ event: "pane-stack", action: "stack-pane" });343 this.bridge({ event: "pane-stack", action: "stack-pane" });
348 this.showToast("Stacked pane");344 this.showToast("Stacked pane");
349 this.exitPaneMode("stack-pane");345 this.exitPaneMode("stack-pane");
M internal/app/browser/focus_manager.go +27 -12
@@ -67,9 +67,15 @@ func (fm *FocusManager) SetActivePane(node *paneNode) {
6767 fm.updateVisualState(oldPane, node)
6868
6969 // Step 3: Notify JavaScript bridge
70- fm.notifyJavaScript(node)
70+ fm.notifyJavaScript(oldPane, node)
7171
72- // Step 4: Execute callbacks
72+ // Step 4: Sync app.activePane for window shortcuts
73+ if fm.wm != nil && fm.wm.app != nil && node.pane != nil {
74+ fm.wm.app.activePane = node.pane
75+ log.Printf("[focus-manager] Synced app.activePane to %p", node.pane.webView)
76+ }
77+
78+ // Step 5: Execute callbacks
7379 fm.executeCallbacks(oldPane, node)
7480 }
7581
@@ -171,18 +177,27 @@ func (fm *FocusManager) updateStackVisibility(stackNode *paneNode) {
171177 }
172178
173179 // notifyJavaScript dispatches focus events to the JavaScript bridge
174-func (fm *FocusManager) notifyJavaScript(node *paneNode) {
175- if node.pane == nil || node.pane.webView == nil {
176- return
177- }
178-
179- detail := map[string]any{
180- "hasFocus": true,
181- "paneId": fm.getPaneID(node),
180+func (fm *FocusManager) notifyJavaScript(oldPane, newPane *paneNode) {
181+ // Notify old pane it lost focus
182+ if oldPane != nil && oldPane.pane != nil && oldPane.pane.webView != nil {
183+ oldDetail := map[string]any{
184+ "active": false,
185+ "paneId": fm.getPaneID(oldPane),
186+ }
187+ if err := oldPane.pane.webView.DispatchCustomEvent("dumber:workspace-focus", oldDetail); err != nil {
188+ log.Printf("[focus-manager] Failed to dispatch blur event: %v", err)
189+ }
182190 }
183191
184- if err := node.pane.webView.DispatchCustomEvent("dumber:workspace-focus", detail); err != nil {
185- log.Printf("[focus-manager] Failed to dispatch focus event: %v", err)
192+ // Notify new pane it gained focus
193+ if newPane != nil && newPane.pane != nil && newPane.pane.webView != nil {
194+ newDetail := map[string]any{
195+ "active": true,
196+ "paneId": fm.getPaneID(newPane),
197+ }
198+ if err := newPane.pane.webView.DispatchCustomEvent("dumber:workspace-focus", newDetail); err != nil {
199+ log.Printf("[focus-manager] Failed to dispatch focus event: %v", err)
200+ }
186201 }
187202 }
188203
@@ -67,9 +67,15 @@ func (fm *FocusManager) SetActivePane(node *paneNode) {
67 fm.updateVisualState(oldPane, node)67 fm.updateVisualState(oldPane, node)
68 68
69 // Step 3: Notify JavaScript bridge69 // Step 3: Notify JavaScript bridge
70- fm.notifyJavaScript(node)70+ fm.notifyJavaScript(oldPane, node)
71 71
72- // Step 4: Execute callbacks72+ // Step 4: Sync app.activePane for window shortcuts
73+ if fm.wm != nil && fm.wm.app != nil && node.pane != nil {
74+ fm.wm.app.activePane = node.pane
75+ log.Printf("[focus-manager] Synced app.activePane to %p", node.pane.webView)
76+ }
77+
78+ // Step 5: Execute callbacks
73 fm.executeCallbacks(oldPane, node)79 fm.executeCallbacks(oldPane, node)
74 }80 }
75 81
@@ -171,18 +177,27 @@ func (fm *FocusManager) updateStackVisibility(stackNode *paneNode) {
171 }177 }
172 178
173 // notifyJavaScript dispatches focus events to the JavaScript bridge179 // notifyJavaScript dispatches focus events to the JavaScript bridge
174-func (fm *FocusManager) notifyJavaScript(node *paneNode) {180+func (fm *FocusManager) notifyJavaScript(oldPane, newPane *paneNode) {
175- if node.pane == nil || node.pane.webView == nil {181+ // Notify old pane it lost focus
176- return182+ if oldPane != nil && oldPane.pane != nil && oldPane.pane.webView != nil {
177- }183+ oldDetail := map[string]any{
178-184+ "active": false,
179- detail := map[string]any{185+ "paneId": fm.getPaneID(oldPane),
180- "hasFocus": true,186+ }
181- "paneId": fm.getPaneID(node),187+ if err := oldPane.pane.webView.DispatchCustomEvent("dumber:workspace-focus", oldDetail); err != nil {
188+ log.Printf("[focus-manager] Failed to dispatch blur event: %v", err)
189+ }
182 }190 }
183 191
184- if err := node.pane.webView.DispatchCustomEvent("dumber:workspace-focus", detail); err != nil {192+ // Notify new pane it gained focus
185- log.Printf("[focus-manager] Failed to dispatch focus event: %v", err)193+ if newPane != nil && newPane.pane != nil && newPane.pane.webView != nil {
194+ newDetail := map[string]any{
195+ "active": true,
196+ "paneId": fm.getPaneID(newPane),
197+ }
198+ if err := newPane.pane.webView.DispatchCustomEvent("dumber:workspace-focus", newDetail); err != nil {
199+ log.Printf("[focus-manager] Failed to dispatch focus event: %v", err)
200+ }
186 }201 }
187 }202 }
188 203
M internal/app/browser/stacked_panes.go +16 -20
@@ -285,18 +285,13 @@ func (spm *StackedPaneManager) finalizeStackCreation(stackNode, newLeaf *paneNod
285285 // Mark stack operation timestamp to prevent focus conflicts
286286 spm.wm.lastStackOperation = time.Now()
287287
288- // Update stack visibility to show current state (existing pane still active)
288+ // Immediately switch to the new pane (don't use deprecated IdleAdd)
289+ stackNode.activeStackIndex = insertIndex
289290 spm.UpdateStackVisibility(stackNode)
290291
291- // Transition to the new pane after a brief delay to avoid rendering conflicts
292- webkit.IdleAdd(func() bool {
293- // Now switch to the new pane
294- stackNode.activeStackIndex = insertIndex
295- spm.UpdateStackVisibility(stackNode)
296- spm.wm.currentlyFocused = newLeaf
297- spm.wm.focusManager.SetActivePane(newLeaf)
298- return false // Remove idle callback
299- })
292+ // Set focus on the new pane synchronously
293+ spm.wm.focusManager.SetActivePane(newLeaf)
294+ spm.wm.currentlyFocused = newLeaf
300295
301296 log.Printf("[workspace] stacked new pane: stackNode=%p newLeaf=%p stackSize=%d activeIndex=%d insertIndex=%d",
302297 stackNode, newLeaf, len(stackNode.stackedPanes), stackNode.activeStackIndex, insertIndex)
@@ -331,16 +326,17 @@ func (spm *StackedPaneManager) UpdateStackVisibility(stackNode *paneNode) {
331326
332327 log.Printf("[workspace] updating stack visibility: activeIndex=%d stackSize=%d", activeIndex, len(stackNode.stackedPanes))
333328
334- // First, ensure the active pane is visible (prevents rendering gaps)
335- activePane := stackNode.stackedPanes[activeIndex]
336- webkit.WidgetSetVisible(activePane.container, true)
337- webkit.WidgetSetVisible(activePane.titleBar, false)
338- webkit.WidgetAddCSSClass(activePane.container, "stacked-pane-active")
339- webkit.WidgetRemoveCSSClass(activePane.container, "stacked-pane-collapsed")
340-
341- // Then hide other panes and show their title bars
329+ // CRITICAL: Process ALL panes in a single pass to prevent flickering
342330 for i, pane := range stackNode.stackedPanes {
343- if i != activeIndex {
331+ if i == activeIndex {
332+ // Active pane: show container, NEVER show title bar
333+ webkit.WidgetSetVisible(pane.container, true)
334+ webkit.WidgetSetVisible(pane.titleBar, false) // ABSOLUTE RULE: never visible for active pane
335+ webkit.WidgetAddCSSClass(pane.container, "stacked-pane-active")
336+ webkit.WidgetRemoveCSSClass(pane.container, "stacked-pane-collapsed")
337+ log.Printf("[workspace] active pane %d: container=visible, titleBar=HIDDEN", i)
338+ } else {
339+ // Inactive panes: hide container, show title bar
344340 webkit.WidgetSetVisible(pane.container, false)
345341 webkit.WidgetSetVisible(pane.titleBar, true)
346342 webkit.WidgetAddCSSClass(pane.container, "stacked-pane-collapsed")
@@ -409,8 +405,8 @@ func (spm *StackedPaneManager) NavigateStack(direction string) bool {
409405
410406 // Focus the new active pane
411407 newActivePane := stackNode.stackedPanes[newIndex]
412- spm.wm.currentlyFocused = newActivePane
413408 spm.wm.focusManager.SetActivePane(newActivePane)
409+ spm.wm.currentlyFocused = newActivePane
414410
415411 log.Printf("[workspace] navigated stack: direction=%s from=%d to=%d stackSize=%d",
416412 direction, currentIndex, newIndex, len(stackNode.stackedPanes))
@@ -285,18 +285,13 @@ func (spm *StackedPaneManager) finalizeStackCreation(stackNode, newLeaf *paneNod
285 // Mark stack operation timestamp to prevent focus conflicts285 // Mark stack operation timestamp to prevent focus conflicts
286 spm.wm.lastStackOperation = time.Now()286 spm.wm.lastStackOperation = time.Now()
287 287
288- // Update stack visibility to show current state (existing pane still active)288+ // Immediately switch to the new pane (don't use deprecated IdleAdd)
289+ stackNode.activeStackIndex = insertIndex
289 spm.UpdateStackVisibility(stackNode)290 spm.UpdateStackVisibility(stackNode)
290 291
291- // Transition to the new pane after a brief delay to avoid rendering conflicts292+ // Set focus on the new pane synchronously
292- webkit.IdleAdd(func() bool {293+ spm.wm.focusManager.SetActivePane(newLeaf)
293- // Now switch to the new pane294+ spm.wm.currentlyFocused = newLeaf
294- stackNode.activeStackIndex = insertIndex
295- spm.UpdateStackVisibility(stackNode)
296- spm.wm.currentlyFocused = newLeaf
297- spm.wm.focusManager.SetActivePane(newLeaf)
298- return false // Remove idle callback
299- })
300 295
301 log.Printf("[workspace] stacked new pane: stackNode=%p newLeaf=%p stackSize=%d activeIndex=%d insertIndex=%d",296 log.Printf("[workspace] stacked new pane: stackNode=%p newLeaf=%p stackSize=%d activeIndex=%d insertIndex=%d",
302 stackNode, newLeaf, len(stackNode.stackedPanes), stackNode.activeStackIndex, insertIndex)297 stackNode, newLeaf, len(stackNode.stackedPanes), stackNode.activeStackIndex, insertIndex)
@@ -331,16 +326,17 @@ func (spm *StackedPaneManager) UpdateStackVisibility(stackNode *paneNode) {
331 326
332 log.Printf("[workspace] updating stack visibility: activeIndex=%d stackSize=%d", activeIndex, len(stackNode.stackedPanes))327 log.Printf("[workspace] updating stack visibility: activeIndex=%d stackSize=%d", activeIndex, len(stackNode.stackedPanes))
333 328
334- // First, ensure the active pane is visible (prevents rendering gaps)329+ // CRITICAL: Process ALL panes in a single pass to prevent flickering
335- activePane := stackNode.stackedPanes[activeIndex]
336- webkit.WidgetSetVisible(activePane.container, true)
337- webkit.WidgetSetVisible(activePane.titleBar, false)
338- webkit.WidgetAddCSSClass(activePane.container, "stacked-pane-active")
339- webkit.WidgetRemoveCSSClass(activePane.container, "stacked-pane-collapsed")
340-
341- // Then hide other panes and show their title bars
342 for i, pane := range stackNode.stackedPanes {330 for i, pane := range stackNode.stackedPanes {
343- if i != activeIndex {331+ if i == activeIndex {
332+ // Active pane: show container, NEVER show title bar
333+ webkit.WidgetSetVisible(pane.container, true)
334+ webkit.WidgetSetVisible(pane.titleBar, false) // ABSOLUTE RULE: never visible for active pane
335+ webkit.WidgetAddCSSClass(pane.container, "stacked-pane-active")
336+ webkit.WidgetRemoveCSSClass(pane.container, "stacked-pane-collapsed")
337+ log.Printf("[workspace] active pane %d: container=visible, titleBar=HIDDEN", i)
338+ } else {
339+ // Inactive panes: hide container, show title bar
344 webkit.WidgetSetVisible(pane.container, false)340 webkit.WidgetSetVisible(pane.container, false)
345 webkit.WidgetSetVisible(pane.titleBar, true)341 webkit.WidgetSetVisible(pane.titleBar, true)
346 webkit.WidgetAddCSSClass(pane.container, "stacked-pane-collapsed")342 webkit.WidgetAddCSSClass(pane.container, "stacked-pane-collapsed")
@@ -409,8 +405,8 @@ func (spm *StackedPaneManager) NavigateStack(direction string) bool {
409 405
410 // Focus the new active pane406 // Focus the new active pane
411 newActivePane := stackNode.stackedPanes[newIndex]407 newActivePane := stackNode.stackedPanes[newIndex]
412- spm.wm.currentlyFocused = newActivePane
413 spm.wm.focusManager.SetActivePane(newActivePane)408 spm.wm.focusManager.SetActivePane(newActivePane)
409+ spm.wm.currentlyFocused = newActivePane
414 410
415 log.Printf("[workspace] navigated stack: direction=%s from=%d to=%d stackSize=%d",411 log.Printf("[workspace] navigated stack: direction=%s from=%d to=%d stackSize=%d",
416 direction, currentIndex, newIndex, len(stackNode.stackedPanes))412 direction, currentIndex, newIndex, len(stackNode.stackedPanes))

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