detached 8fbbe5e

fix(cef): bind permission dialog for floating panes

Resolve the owning window with browserWindowForAnyPane for both the
permission dialog binding and the WebRTC indicator. When no window owns the
pane the presenter is cleared so the request is denied instead of being shown
in a stale or wrong window.
commit
8fbbe5e19fab1b03610ce2a91ffb1246c2e8b01a
author
brice · · 2026-10-03 07:04 UTC
committer
brice · · 2026-10-03 07:50 UTC
parent
45bd51f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +41 -4
M internal/ui/app.go +14 -4
@@ -3589,6 +3589,16 @@ func (a *App) wireKeyboardActions() {
35893589 })
35903590 }
35913591
3592+// permissionDialogForPane returns the permission dialog of the window owning the
3593+// pane (including floating panes), or nil when no window or dialog is available.
3594+func (a *App) permissionDialogForPane(paneID entity.PaneID) port.PermissionDialogPresenter {
3595+ bw := a.browserWindowForAnyPane(paneID)
3596+ if bw == nil || bw.permissionDialog == nil {
3597+ return nil
3598+ }
3599+ return bw.permissionDialog
3600+}
3601+
35923602 func (a *App) wireWebRTCPermissionIndicator() {
35933603 if a.contentCoord == nil {
35943604 return
@@ -3602,9 +3612,9 @@ func (a *App) wireWebRTCPermissionIndicator() {
36023612 if a.deps == nil || a.deps.PermissionUC == nil {
36033613 return
36043614 }
3605- if bw := a.browserWindowForPane(paneID); bw != nil && bw.permissionDialog != nil {
3606- a.deps.PermissionUC.SetDialogPresenter(bw.permissionDialog)
3607- }
3615+ // Clears the presenter when no window owns the pane, so the request is
3616+ // denied instead of being shown in a stale or wrong window.
3617+ a.deps.PermissionUC.SetDialogPresenter(a.permissionDialogForPane(paneID))
36083618 })
36093619
36103620 a.contentCoord.SetOnPermissionActivity(func(
@@ -3613,7 +3623,7 @@ func (a *App) wireWebRTCPermissionIndicator() {
36133623 permTypes []entity.PermissionType,
36143624 state content.PermissionActivityState,
36153625 ) {
3616- bw := a.browserWindowForPane(paneID)
3626+ bw := a.browserWindowForAnyPane(paneID)
36173627 if bw == nil || bw.webrtcIndicator == nil {
36183628 return
36193629 }
@@ -3589,6 +3589,16 @@ func (a *App) wireKeyboardActions() {
3589 })3589 })
3590 }3590 }
3591 3591
3592+// permissionDialogForPane returns the permission dialog of the window owning the
3593+// pane (including floating panes), or nil when no window or dialog is available.
3594+func (a *App) permissionDialogForPane(paneID entity.PaneID) port.PermissionDialogPresenter {
3595+ bw := a.browserWindowForAnyPane(paneID)
3596+ if bw == nil || bw.permissionDialog == nil {
3597+ return nil
3598+ }
3599+ return bw.permissionDialog
3600+}
3601+
3592 func (a *App) wireWebRTCPermissionIndicator() {3602 func (a *App) wireWebRTCPermissionIndicator() {
3593 if a.contentCoord == nil {3603 if a.contentCoord == nil {
3594 return3604 return
@@ -3602,9 +3612,9 @@ func (a *App) wireWebRTCPermissionIndicator() {
3602 if a.deps == nil || a.deps.PermissionUC == nil {3612 if a.deps == nil || a.deps.PermissionUC == nil {
3603 return3613 return
3604 }3614 }
3605- if bw := a.browserWindowForPane(paneID); bw != nil && bw.permissionDialog != nil {3615+ // Clears the presenter when no window owns the pane, so the request is
3606- a.deps.PermissionUC.SetDialogPresenter(bw.permissionDialog)3616+ // denied instead of being shown in a stale or wrong window.
3607- }3617+ a.deps.PermissionUC.SetDialogPresenter(a.permissionDialogForPane(paneID))
3608 })3618 })
3609 3619
3610 a.contentCoord.SetOnPermissionActivity(func(3620 a.contentCoord.SetOnPermissionActivity(func(
@@ -3613,7 +3623,7 @@ func (a *App) wireWebRTCPermissionIndicator() {
3613 permTypes []entity.PermissionType,3623 permTypes []entity.PermissionType,
3614 state content.PermissionActivityState,3624 state content.PermissionActivityState,
3615 ) {3625 ) {
3616- bw := a.browserWindowForPane(paneID)3626+ bw := a.browserWindowForAnyPane(paneID)
3617 if bw == nil || bw.webrtcIndicator == nil {3627 if bw == nil || bw.webrtcIndicator == nil {
3618 return3628 return
3619 }3629 }
M internal/ui/browser_window_test.go +27 -0
@@ -1159,3 +1159,30 @@ func TestRestoreSession_ActiveWindowIndexSyncsState(t *testing.T) {
11591159 assert.Equal(t, entity.WindowID("active-w2"), result[idx].WindowID,
11601160 "window at active index must match focused window ID")
11611161 }
1162+
1163+func TestApp_PermissionDialogForPane(t *testing.T) {
1164+ tabID := entity.TabID("tab-1")
1165+ tabPane := entity.PaneID("pane-in-tab")
1166+ floatingPane := entity.PaneID("pane-floating")
1167+ tab := entity.NewTab(tabID, entity.WorkspaceID("ws-1"), entity.NewPane(tabPane))
1168+ tabs := entity.NewTabList()
1169+ tabs.Add(tab)
1170+ dialogA := &testPermissionDialogPresenter{}
1171+ owner := &browserWindow{id: "window-1", tabs: tabs, permissionDialog: dialogA}
1172+ noDialog := &browserWindow{id: "window-2", tabs: entity.NewTabList()}
1173+ app := &App{
1174+ browserWindows: map[string]*browserWindow{owner.id: owner, noDialog.id: noDialog},
1175+ windowForTab: map[entity.TabID]*browserWindow{tabID: owner},
1176+ floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{
1177+ {tabID: tabID, sessionID: "profile"}: {paneID: floatingPane},
1178+ },
1179+ }
1180+
1181+ assert.Same(t, dialogA, app.permissionDialogForPane(tabPane), "tiled pane")
1182+ assert.Same(t, dialogA, app.permissionDialogForPane(floatingPane), "floating pane uses its tab's window")
1183+ assert.Nil(t, app.permissionDialogForPane("unknown"), "unowned pane clears the presenter")
1184+ assert.Nil(t, app.permissionDialogForPane(""))
1185+
1186+ owner.permissionDialog = nil
1187+ assert.Nil(t, app.permissionDialogForPane(tabPane), "window without dialog")
1188+}
@@ -1159,3 +1159,30 @@ func TestRestoreSession_ActiveWindowIndexSyncsState(t *testing.T) {
1159 assert.Equal(t, entity.WindowID("active-w2"), result[idx].WindowID,1159 assert.Equal(t, entity.WindowID("active-w2"), result[idx].WindowID,
1160 "window at active index must match focused window ID")1160 "window at active index must match focused window ID")
1161 }1161 }
1162+
1163+func TestApp_PermissionDialogForPane(t *testing.T) {
1164+ tabID := entity.TabID("tab-1")
1165+ tabPane := entity.PaneID("pane-in-tab")
1166+ floatingPane := entity.PaneID("pane-floating")
1167+ tab := entity.NewTab(tabID, entity.WorkspaceID("ws-1"), entity.NewPane(tabPane))
1168+ tabs := entity.NewTabList()
1169+ tabs.Add(tab)
1170+ dialogA := &testPermissionDialogPresenter{}
1171+ owner := &browserWindow{id: "window-1", tabs: tabs, permissionDialog: dialogA}
1172+ noDialog := &browserWindow{id: "window-2", tabs: entity.NewTabList()}
1173+ app := &App{
1174+ browserWindows: map[string]*browserWindow{owner.id: owner, noDialog.id: noDialog},
1175+ windowForTab: map[entity.TabID]*browserWindow{tabID: owner},
1176+ floatingSessions: map[floatingSessionKey]*floatingWorkspaceSession{
1177+ {tabID: tabID, sessionID: "profile"}: {paneID: floatingPane},
1178+ },
1179+ }
1180+
1181+ assert.Same(t, dialogA, app.permissionDialogForPane(tabPane), "tiled pane")
1182+ assert.Same(t, dialogA, app.permissionDialogForPane(floatingPane), "floating pane uses its tab's window")
1183+ assert.Nil(t, app.permissionDialogForPane("unknown"), "unowned pane clears the presenter")
1184+ assert.Nil(t, app.permissionDialogForPane(""))
1185+
1186+ owner.permissionDialog = nil
1187+ assert.Nil(t, app.permissionDialogForPane(tabPane), "window without dialog")
1188+}

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