detached 81d2160

fix: localize multi-window input and permission UI

commit
81d21608c4e04259312e4174a096adc4a729ed8e
author
brice · · 2026-04-15 02:56 UTC
parent
201b740
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
10 files · +330 -115
M cmd/dumber/main_test.go +15 -0
@@ -161,3 +161,18 @@ func TestTryForwardBrowseURLToRunningInstance_ReturnsFalseOnRelayMiss(t *testing
161161 t.Fatal("expected browse URL to remain unforwarded")
162162 }
163163 }
164+
165+func TestTryForwardBrowseURLToRunningInstance_PropagatesError(t *testing.T) {
166+ relay := mocks.NewMockBrowserLaunchRelay(t)
167+ wantErr := errors.New("relay error")
168+ relay.EXPECT().DeliverOpenFreshWindow(context.Background(), "https://example.com").Return(false, wantErr)
169+
170+ forwarded, err := tryForwardBrowseURLToRunningInstance(context.Background(), relay, "https://example.com")
171+
172+ if !errors.Is(err, wantErr) {
173+ t.Fatalf("expected error %v, got %v", wantErr, err)
174+ }
175+ if forwarded {
176+ t.Fatal("expected forwarded to be false on error")
177+ }
178+}
@@ -161,3 +161,18 @@ func TestTryForwardBrowseURLToRunningInstance_ReturnsFalseOnRelayMiss(t *testing
161 t.Fatal("expected browse URL to remain unforwarded")161 t.Fatal("expected browse URL to remain unforwarded")
162 }162 }
163 }163 }
164+
165+func TestTryForwardBrowseURLToRunningInstance_PropagatesError(t *testing.T) {
166+ relay := mocks.NewMockBrowserLaunchRelay(t)
167+ wantErr := errors.New("relay error")
168+ relay.EXPECT().DeliverOpenFreshWindow(context.Background(), "https://example.com").Return(false, wantErr)
169+
170+ forwarded, err := tryForwardBrowseURLToRunningInstance(context.Background(), relay, "https://example.com")
171+
172+ if !errors.Is(err, wantErr) {
173+ t.Fatalf("expected error %v, got %v", wantErr, err)
174+ }
175+ if forwarded {
176+ t.Fatal("expected forwarded to be false on error")
177+ }
178+}
M internal/infrastructure/cef/app_test.go +21 -0
@@ -122,3 +122,24 @@ func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsEmptyBrowseURLAndReturns1
122122 t.Fatalf("forwarded browse url = %q, want empty string", gotURL)
123123 }
124124 }
125+
126+func TestDumberBPH_OnAlreadyRunningAppRelaunch_DoesNotForwardNonBrowse(t *testing.T) {
127+ t.Parallel()
128+
129+ called := false
130+ eng := &Engine{}
131+ eng.SetAlreadyRunningAppRelaunchHandler(func(string) {
132+ called = true
133+ })
134+
135+ ret := (&dumberBPH{engine: eng}).OnAlreadyRunningAppRelaunch(relaunchCommandLineStub{
136+ commandLineString: "dumber",
137+ }, "")
138+
139+ if ret != 1 {
140+ t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 1", ret)
141+ }
142+ if called {
143+ t.Fatal("handler should not be called for non-browse relaunch")
144+ }
145+}
@@ -122,3 +122,24 @@ func TestDumberBPH_OnAlreadyRunningAppRelaunch_ForwardsEmptyBrowseURLAndReturns1
122 t.Fatalf("forwarded browse url = %q, want empty string", gotURL)122 t.Fatalf("forwarded browse url = %q, want empty string", gotURL)
123 }123 }
124 }124 }
125+
126+func TestDumberBPH_OnAlreadyRunningAppRelaunch_DoesNotForwardNonBrowse(t *testing.T) {
127+ t.Parallel()
128+
129+ called := false
130+ eng := &Engine{}
131+ eng.SetAlreadyRunningAppRelaunchHandler(func(string) {
132+ called = true
133+ })
134+
135+ ret := (&dumberBPH{engine: eng}).OnAlreadyRunningAppRelaunch(relaunchCommandLineStub{
136+ commandLineString: "dumber",
137+ }, "")
138+
139+ if ret != 1 {
140+ t.Fatalf("OnAlreadyRunningAppRelaunch returned %d, want 1", ret)
141+ }
142+ if called {
143+ t.Fatal("handler should not be called for non-browse relaunch")
144+ }
145+}
M internal/infrastructure/desktop/browser_launch_relay.go +1 -1
@@ -198,7 +198,7 @@ func (l *browserLaunchRelayListener) serve(ctx context.Context, opener port.Brow
198198 continue
199199 }
200200
201- l.handleConnection(ctx, conn, opener)
201+ go l.handleConnection(ctx, conn, opener)
202202 }
203203 }
204204
@@ -198,7 +198,7 @@ func (l *browserLaunchRelayListener) serve(ctx context.Context, opener port.Brow
198 continue198 continue
199 }199 }
200 200
201- l.handleConnection(ctx, conn, opener)201+ go l.handleConnection(ctx, conn, opener)
202 }202 }
203 }203 }
204 204
M internal/ui/app.go +187 -99
@@ -117,8 +117,6 @@ type App struct {
117117 appToaster *component.Toaster
118118 // Mode indicator toaster for modal mode notifications (pane, tab, session, resize)
119119 modeToaster *component.Toaster
120- // Top-right WebRTC permission activity indicator
121- webrtcIndicator *component.WebRTCPermissionIndicator
122120
123121 // Session management
124122 sessionManager *component.SessionManager
@@ -433,8 +431,7 @@ func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*brow
433431 if w := permPopup.Widget(); w != nil {
434432 mainWindow.AddOverlay(w)
435433 }
436- permDialog := dialog.NewPermissionDialog(permPopup)
437- a.deps.PermissionUC.SetDialogPresenter(permDialog)
434+ browserWindow.permissionDialog = dialog.NewPermissionDialog(permPopup)
438435 }
439436 }
440437
@@ -443,8 +440,15 @@ func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*brow
443440 if indicator != nil {
444441 if w := indicator.Widget(); w != nil {
445442 mainWindow.AddOverlay(w)
446- a.webrtcIndicator = indicator
447443 }
444+ browserWindow.webrtcIndicator = indicator
445+ }
446+ a.wireBrowserWindowPermissionIndicator(browserWindow)
447+ if a.kbDispatcher != nil {
448+ a.initBrowserWindowInput(ctx, browserWindow)
449+ }
450+ if a.tabCoord != nil {
451+ a.wireBrowserWindowTabBar(ctx, browserWindow)
448452 }
449453
450454 // Apply GTK CSS styling from theme manager.
@@ -487,12 +491,47 @@ func (a *App) browserWindowForTab(tabID entity.TabID) *browserWindow {
487491 return a.browserWindowForMainWindow(a.mainWindow)
488492 }
489493
494+func (a *App) browserWindowForPane(paneID entity.PaneID) *browserWindow {
495+ if paneID == "" || a.tabs == nil {
496+ return nil
497+ }
498+ for _, tab := range a.tabs.Tabs {
499+ if tab == nil || tab.Workspace == nil {
500+ continue
501+ }
502+ if tab.Workspace.FindPane(paneID) != nil {
503+ return a.browserWindowForTab(tab.ID)
504+ }
505+ }
506+ return nil
507+}
508+
509+func (a *App) activateBrowserWindow(bw *browserWindow) {
510+ if bw == nil {
511+ return
512+ }
513+ if bw.mainWindow != nil {
514+ a.mainWindow = bw.mainWindow
515+ }
516+ a.keyboardHandler = bw.keyboardHandler
517+ a.globalShortcutHandler = bw.globalShortcutHandler
518+ if a.tabCoord != nil && bw.mainWindow != nil {
519+ a.tabCoord.SetMainWindow(bw.mainWindow)
520+ }
521+ if a.tabs != nil && bw.activeTabID != "" && a.tabs.Find(bw.activeTabID) != nil {
522+ a.tabs.SetActive(bw.activeTabID)
523+ }
524+}
525+
490526 func (a *App) setBrowserWindowForTab(tabID entity.TabID, bw *browserWindow) {
491527 if bw == nil {
492528 return
493529 }
494530 a.ensureWindowForTabMap()
495531 a.windowForTab[tabID] = bw
532+ if bw.activeTabID == "" {
533+ bw.activeTabID = tabID
534+ }
496535 }
497536
498537 func (a *App) startBrowserLaunchRelayListener(ctx context.Context) {
@@ -525,18 +564,24 @@ func (a *App) openFreshWindow(ctx context.Context, url string) error {
525564 a.registerBrowserWindow(created)
526565
527566 if a.tabs == nil || a.tabs.Count() == 0 {
567+ a.activateBrowserWindow(created)
528568 return nil
529569 }
530570
571+ var openErr error
531572 if a.tabCoord != nil {
532- return a.openFreshWindowWithTabCoord(ctx, url, created)
533- }
534-
535- if a.tabsUC == nil {
573+ openErr = a.openFreshWindowWithTabCoord(ctx, url, created)
574+ } else if a.tabsUC == nil {
575+ a.activateBrowserWindow(created)
536576 return nil
577+ } else {
578+ openErr = a.openFreshWindowWithTabsUC(ctx, url, created)
537579 }
538-
539- return a.openFreshWindowWithTabsUC(ctx, url, created)
580+ if openErr != nil {
581+ return openErr
582+ }
583+ a.activateBrowserWindow(created)
584+ return nil
540585 }
541586
542587 func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error {
@@ -579,6 +624,7 @@ func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, creat
579624 return fmt.Errorf("workspace view not created for tab %s", createdTab.ID)
580625 }
581626 a.setBrowserWindowForTab(createdTab.ID, created)
627+ created.activeTabID = createdTab.ID
582628 if created.mainWindow != nil {
583629 created.mainWindow.Show()
584630 }
@@ -591,9 +637,14 @@ func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created
591637 InitialURL: url,
592638 })
593639 if err != nil {
640+ a.removeBrowserWindow(created.id)
641+ if created.mainWindow != nil {
642+ created.mainWindow.Destroy()
643+ }
594644 return err
595645 }
596646 a.setBrowserWindowForTab(output.Tab.ID, created)
647+ created.activeTabID = output.Tab.ID
597648 if a.widgetFactory != nil {
598649 a.tabCreationWindow = created
599650 defer func() {
@@ -832,37 +883,39 @@ func (a *autoCopyConfigFn) IsAutoCopyEnabled() bool {
832883 }
833884
834885 func (a *App) initKeyboardHandler(ctx context.Context) {
886+ bw := a.browserWindowForMainWindow(a.mainWindow)
887+ if bw == nil {
888+ return
889+ }
890+ a.initBrowserWindowInput(ctx, bw)
891+}
892+
893+func (a *App) initBrowserWindowInput(ctx context.Context, bw *browserWindow) {
835894 log := logging.FromContext(ctx)
836895
837- if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil {
896+ if bw == nil || bw.mainWindow == nil || a.deps == nil || a.deps.Config == nil || a.kbDispatcher == nil {
838897 return
839898 }
840899
841- // Create keyboard handler and wire to dispatcher.
842- a.keyboardHandler = input.NewKeyboardHandler(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)
843- a.keyboardHandler.SetOnAction(func(ctx context.Context, action input.Action) error {
900+ bw.keyboardHandler = input.NewKeyboardHandler(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)
901+ bw.keyboardHandler.SetOnAction(func(actionCtx context.Context, action input.Action) error {
902+ a.activateBrowserWindow(bw)
844903 if action == input.ActionClosePane {
845- if a.closeAndReleaseActiveFloatingPane(ctx) {
904+ if a.closeAndReleaseActiveFloatingPane(actionCtx) {
846905 return nil
847906 }
848907 }
849- return a.kbDispatcher.Dispatch(ctx, action)
908+ return a.kbDispatcher.Dispatch(actionCtx, action)
850909 })
851- a.keyboardHandler.SetOnEscape(func(ctx context.Context) bool {
852- return a.handleGlobalEscape(ctx)
910+ bw.keyboardHandler.SetOnEscape(func(escapeCtx context.Context) bool {
911+ a.activateBrowserWindow(bw)
912+ return a.handleGlobalEscape(escapeCtx)
853913 })
854- a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) {
914+ bw.keyboardHandler.SetOnModeChange(func(from, to input.Mode) {
915+ a.activateBrowserWindow(bw)
855916 a.handleModeChange(ctx, from, to)
856917 })
857- // Wire key routing policy.
858- // Determines how each key event should be handled based on UI state:
859- // - Overlays visible (session manager, tab picker): pass all keys to widget
860- // - Omnibox visible: accent detection for text keys, shortcuts for modified keys
861- // - WebView focused: pass text/dead keys to WebKit IM for native compose,
862- // intercept shortcut-modified keys (Ctrl+, Alt+)
863- // - Default: handle through shortcut system
864- a.keyboardHandler.SetRouteKey(func(kc input.KeyContext) input.KeyRoute {
865- // Overlays take full keyboard control
918+ bw.keyboardHandler.SetRouteKey(func(kc input.KeyContext) input.KeyRoute {
866919 if a.sessionManager != nil && a.sessionManager.IsVisible() {
867920 return input.RoutePassToWidget
868921 }
@@ -870,19 +923,8 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
870923 return input.RoutePassToWidget
871924 }
872925
873- wsView := a.activeWorkspaceView()
874- if wsView == nil {
875- return input.RouteHandleShortcuts
876- }
877-
878- // Check actual focus rather than visibility: find bar can remain
879- // visible after focus returns to the WebView, so visibility alone
880- // would misroute keys.
881926 if a.accentFocusProvider != nil {
882927 if _, ok := a.accentFocusProvider.GetFocusedInput().(port.EntryInputTarget); ok {
883- // GTK Entry focused (omnibox or find bar): Alt-modified keys go to
884- // shortcuts (pane navigation), other keys pass through to the widget's
885- // own capture-phase key controller (which handles accent detection)
886928 if kc.Modifiers&input.ModAlt != 0 {
887929 return input.RouteHandleShortcuts
888930 }
@@ -890,8 +932,6 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
890932 }
891933 }
892934
893- // WebView focused: text/dead keys pass through for native IM compose,
894- // shortcut-modified keys (Ctrl+, Alt+) are handled by shortcut system
895935 if input.IsShortcutModified(kc.Modifiers) {
896936 return input.RouteHandleShortcuts
897937 }
@@ -899,31 +939,43 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
899939 return input.RoutePassToWidget
900940 }
901941
902- // Non-text, non-shortcut keys (F-keys, arrows, etc.): shortcut system
903942 return input.RouteHandleShortcuts
904943 })
905- // Wire accent handler for dead keys support
906944 if a.insertAccentUC != nil {
907- a.keyboardHandler.SetAccentHandler(a.insertAccentUC)
945+ bw.keyboardHandler.SetAccentHandler(a.insertAccentUC)
908946 }
909- a.keyboardHandler.AttachTo(a.mainWindow.Window())
947+ bw.keyboardHandler.AttachTo(bw.mainWindow.Window())
910948
911- // Create global shortcut handler for Alt+1-9 tab switching.
912- // This uses GtkShortcutController with GTK_SHORTCUT_SCOPE_GLOBAL
913- // to intercept shortcuts even when WebView has focus.
914- a.globalShortcutHandler = input.NewGlobalShortcutHandler(
949+ bw.globalShortcutHandler = input.NewGlobalShortcutHandler(
915950 ctx,
916- a.mainWindow.Window(),
951+ bw.mainWindow.Window(),
917952 &a.deps.Config.Workspace,
918953 &a.deps.Config.Session,
919- a.keyboardHandler,
920- func(ctx context.Context, action input.Action) error {
921- return a.kbDispatcher.Dispatch(ctx, action)
954+ bw.keyboardHandler,
955+ func(actionCtx context.Context, action input.Action) error {
956+ a.activateBrowserWindow(bw)
957+ return a.kbDispatcher.Dispatch(actionCtx, action)
922958 },
923959 )
924- if a.globalShortcutHandler == nil {
925- log.Warn().Msg("global shortcut handler creation failed, shortcuts may not work when WebView has focus")
960+ if bw.globalShortcutHandler == nil {
961+ log.Warn().Str("window_id", bw.id).Msg("global shortcut handler creation failed, shortcuts may not work when WebView has focus")
926962 }
963+
964+ a.activateBrowserWindow(bw)
965+}
966+
967+func (a *App) wireBrowserWindowTabBar(ctx context.Context, bw *browserWindow) {
968+ if bw == nil || bw.mainWindow == nil || bw.mainWindow.TabBar() == nil || a.tabCoord == nil {
969+ return
970+ }
971+ bw.mainWindow.TabBar().SetOnSwitch(func(tabID entity.TabID) {
972+ a.activateBrowserWindow(bw)
973+ if err := a.tabCoord.Switch(ctx, tabID); err != nil {
974+ logging.FromContext(ctx).Error().Err(err).Str("tab_id", string(tabID)).Str("window_id", bw.id).Msg("tab switch failed")
975+ return
976+ }
977+ bw.activeTabID = tabID
978+ })
927979 }
928980
929981 // handleAccentKeyPress handles accent key press events for GTK entry widgets
@@ -1849,6 +1901,10 @@ func (a *App) initCoordinators(ctx context.Context) {
18491901 a.createWorkspaceView(ctx, tab)
18501902 })
18511903 a.tabCoord.SetOnTabSwitched(func(ctx context.Context, tab *entity.Tab) {
1904+ if bw := a.browserWindowForTab(tab.ID); bw != nil {
1905+ bw.activeTabID = tab.ID
1906+ a.activateBrowserWindow(bw)
1907+ }
18521908 a.switchWorkspaceView(ctx, tab.ID)
18531909 })
18541910 a.tabCoord.SetOnQuit(a.Quit)
@@ -1858,13 +1914,8 @@ func (a *App) initCoordinators(ctx context.Context) {
18581914 a.attachPopupToTab(ctx, tabID, pane, wv)
18591915 })
18601916
1861- // Wire tab bar click handling to coordinator
1862- if a.mainWindow != nil && a.mainWindow.TabBar() != nil {
1863- a.mainWindow.TabBar().SetOnSwitch(func(tabID entity.TabID) {
1864- if err := a.tabCoord.Switch(ctx, tabID); err != nil {
1865- log.Error().Err(err).Str("tab_id", string(tabID)).Msg("tab switch failed")
1866- }
1867- })
1917+ for _, bw := range a.browserWindows {
1918+ a.wireBrowserWindowTabBar(ctx, bw)
18681919 }
18691920
18701921 // Set fullscreen callback to hide/show tab bar (after tabCoord is initialized)
@@ -1999,6 +2050,9 @@ func (a *App) initCoordinators(ctx context.Context) {
19992050 a.deps.Config.Workspace.NewPaneURL,
20002051 )
20012052 a.wireKeyboardActions()
2053+ for _, bw := range a.browserWindows {
2054+ a.initBrowserWindowInput(ctx, bw)
2055+ }
20022056
20032057 log.Debug().Msg("coordinators initialized")
20042058 }
@@ -2041,33 +2095,73 @@ func (a *App) wireKeyboardActions() {
20412095 }
20422096
20432097 func (a *App) wireWebRTCPermissionIndicator() {
2044- if a.webrtcIndicator == nil || a.contentCoord == nil {
2098+ if a.contentCoord == nil {
20452099 return
20462100 }
20472101 ctx := context.Background()
20482102 if a.deps != nil && a.deps.Ctx != nil {
20492103 ctx = a.deps.Ctx
20502104 }
2051- log := logging.FromContext(ctx)
20522105
2053- a.contentCoord.SetOnPermissionActivity(func(origin string, permTypes []entity.PermissionType, state content.PermissionActivityState) {
2054- a.webrtcIndicator.SetOrigin(origin)
2106+ a.contentCoord.SetOnPermissionActivity(func(
2107+ paneID entity.PaneID,
2108+ origin string,
2109+ permTypes []entity.PermissionType,
2110+ state content.PermissionActivityState,
2111+ ) {
2112+ bw := a.browserWindowForPane(paneID)
2113+ if bw == nil || bw.webrtcIndicator == nil {
2114+ return
2115+ }
2116+ if state == content.PermissionActivityRequesting && a.deps != nil && a.deps.PermissionUC != nil && bw.permissionDialog != nil {
2117+ a.deps.PermissionUC.SetDialogPresenter(bw.permissionDialog)
2118+ }
2119+ bw.webrtcIndicator.SetOrigin(origin)
20552120
20562121 switch state {
20572122 case content.PermissionActivityRequesting:
2058- a.webrtcIndicator.MarkRequesting(permTypes)
2123+ bw.webrtcIndicator.MarkRequesting(permTypes)
20592124 case content.PermissionActivityAllowed:
2060- a.webrtcIndicator.MarkAllowed(permTypes)
2125+ bw.webrtcIndicator.MarkAllowed(permTypes)
20612126 case content.PermissionActivityBlocked:
2062- a.webrtcIndicator.MarkBlocked(permTypes)
2127+ bw.webrtcIndicator.MarkBlocked(permTypes)
20632128 }
20642129
20652130 for _, permType := range permTypes {
2066- a.syncWebRTCPermissionLockState(ctx, origin, permType)
2131+ a.syncWebRTCPermissionLockState(ctx, bw.webrtcIndicator, origin, permType)
20672132 }
20682133 })
20692134
2070- a.webrtcIndicator.SetOnToggleLock(func(origin string, permType entity.PermissionType, state string, hasStored bool) {
2135+ // Reset the owning window's indicator when that pane navigates away.
2136+ a.contentCoord.SetOnActiveNavigationCommitted(func(paneID entity.PaneID, uri string) {
2137+ bw := a.browserWindowForPane(paneID)
2138+ if bw == nil || bw.webrtcIndicator == nil {
2139+ return
2140+ }
2141+ newOrigin, err := urlutil.ExtractOrigin(uri)
2142+ if err != nil {
2143+ bw.webrtcIndicator.Reset()
2144+ return
2145+ }
2146+
2147+ currentOrigin := bw.webrtcIndicator.Origin()
2148+ if currentOrigin != "" && currentOrigin != newOrigin {
2149+ bw.webrtcIndicator.Reset()
2150+ }
2151+ })
2152+}
2153+
2154+func (a *App) wireBrowserWindowPermissionIndicator(bw *browserWindow) {
2155+ if bw == nil || bw.webrtcIndicator == nil {
2156+ return
2157+ }
2158+ ctx := context.Background()
2159+ if a.deps != nil && a.deps.Ctx != nil {
2160+ ctx = a.deps.Ctx
2161+ }
2162+ log := logging.FromContext(ctx)
2163+
2164+ bw.webrtcIndicator.SetOnToggleLock(func(origin string, permType entity.PermissionType, state string, hasStored bool) {
20712165 if a.deps == nil || a.deps.PermissionUC == nil || origin == "" {
20722166 return
20732167 }
@@ -2092,27 +2186,17 @@ func (a *App) wireWebRTCPermissionIndicator() {
20922186 }
20932187 }
20942188
2095- a.syncWebRTCPermissionLockState(ctx, origin, permType)
2096- })
2097-
2098- // Reset indicator when the active pane navigates away from the current origin.
2099- a.contentCoord.SetOnActiveNavigationCommitted(func(uri string) {
2100- newOrigin, err := urlutil.ExtractOrigin(uri)
2101- if err != nil {
2102- // Internal pages (dumb://, about:) — clear the indicator.
2103- a.webrtcIndicator.Reset()
2104- return
2105- }
2106-
2107- currentOrigin := a.webrtcIndicator.Origin()
2108- if currentOrigin != "" && currentOrigin != newOrigin {
2109- a.webrtcIndicator.Reset()
2110- }
2189+ a.syncWebRTCPermissionLockState(ctx, bw.webrtcIndicator, origin, permType)
21112190 })
21122191 }
21132192
2114-func (a *App) syncWebRTCPermissionLockState(ctx context.Context, origin string, permType entity.PermissionType) {
2115- if a.webrtcIndicator == nil || a.deps == nil || a.deps.PermissionUC == nil || origin == "" {
2193+func (a *App) syncWebRTCPermissionLockState(
2194+ ctx context.Context,
2195+ indicator *component.WebRTCPermissionIndicator,
2196+ origin string,
2197+ permType entity.PermissionType,
2198+) {
2199+ if indicator == nil || a.deps == nil || a.deps.PermissionUC == nil || origin == "" {
21162200 return
21172201 }
21182202
@@ -2127,11 +2211,11 @@ func (a *App) syncWebRTCPermissionLockState(ctx context.Context, origin string,
21272211 }
21282212
21292213 if record == nil {
2130- a.webrtcIndicator.SetStoredDecision(permType, entity.PermissionPrompt, false)
2214+ indicator.SetStoredDecision(permType, entity.PermissionPrompt, false)
21312215 return
21322216 }
21332217
2134- a.webrtcIndicator.SetStoredDecision(permType, record.Decision, true)
2218+ indicator.SetStoredDecision(permType, record.Decision, true)
21352219 }
21362220
21372221 // generateWindowID generates a unique ID for top-level browser windows.
@@ -2535,6 +2619,7 @@ func (a *App) switchWorkspaceView(ctx context.Context, tabID entity.TabID) {
25352619
25362620 // Swap content (MainWindow.SetContent now properly removes old content)
25372621 if target := a.browserWindowForTab(tabID); target != nil && target.mainWindow != nil {
2622+ target.activeTabID = tabID
25382623 target.mainWindow.SetContent(gtkWidget)
25392624 } else if a.mainWindow != nil {
25402625 a.mainWindow.SetContent(gtkWidget)
@@ -3411,13 +3496,16 @@ func (a *App) initConfigWatcher(ctx context.Context) {
34113496 a.deps.OnConfigChange(func() {
34123497 cb := glib.SourceFunc(func(_ uintptr) bool {
34133498 a.applyAppearanceConfig(ctx)
3414- // Reload keyboard shortcuts
3415- if a.keyboardHandler != nil {
3416- a.keyboardHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)
3417- }
3418- // Reload global shortcuts (removes stale, re-registers from config)
3419- if a.globalShortcutHandler != nil {
3420- a.globalShortcutHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)
3499+ for _, bw := range a.browserWindows {
3500+ if bw == nil {
3501+ continue
3502+ }
3503+ if bw.keyboardHandler != nil {
3504+ bw.keyboardHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)
3505+ }
3506+ if bw.globalShortcutHandler != nil {
3507+ bw.globalShortcutHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)
3508+ }
34213509 }
34223510 return false
34233511 })
@@ -117,8 +117,6 @@ type App struct {
117 appToaster *component.Toaster117 appToaster *component.Toaster
118 // Mode indicator toaster for modal mode notifications (pane, tab, session, resize)118 // Mode indicator toaster for modal mode notifications (pane, tab, session, resize)
119 modeToaster *component.Toaster119 modeToaster *component.Toaster
120- // Top-right WebRTC permission activity indicator
121- webrtcIndicator *component.WebRTCPermissionIndicator
122 120
123 // Session management121 // Session management
124 sessionManager *component.SessionManager122 sessionManager *component.SessionManager
@@ -433,8 +431,7 @@ func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*brow
433 if w := permPopup.Widget(); w != nil {431 if w := permPopup.Widget(); w != nil {
434 mainWindow.AddOverlay(w)432 mainWindow.AddOverlay(w)
435 }433 }
436- permDialog := dialog.NewPermissionDialog(permPopup)434+ browserWindow.permissionDialog = dialog.NewPermissionDialog(permPopup)
437- a.deps.PermissionUC.SetDialogPresenter(permDialog)
438 }435 }
439 }436 }
440 437
@@ -443,8 +440,15 @@ func (a *App) createBrowserWindow(ctx context.Context, initialURL string) (*brow
443 if indicator != nil {440 if indicator != nil {
444 if w := indicator.Widget(); w != nil {441 if w := indicator.Widget(); w != nil {
445 mainWindow.AddOverlay(w)442 mainWindow.AddOverlay(w)
446- a.webrtcIndicator = indicator
447 }443 }
444+ browserWindow.webrtcIndicator = indicator
445+ }
446+ a.wireBrowserWindowPermissionIndicator(browserWindow)
447+ if a.kbDispatcher != nil {
448+ a.initBrowserWindowInput(ctx, browserWindow)
449+ }
450+ if a.tabCoord != nil {
451+ a.wireBrowserWindowTabBar(ctx, browserWindow)
448 }452 }
449 453
450 // Apply GTK CSS styling from theme manager.454 // Apply GTK CSS styling from theme manager.
@@ -487,12 +491,47 @@ func (a *App) browserWindowForTab(tabID entity.TabID) *browserWindow {
487 return a.browserWindowForMainWindow(a.mainWindow)491 return a.browserWindowForMainWindow(a.mainWindow)
488 }492 }
489 493
494+func (a *App) browserWindowForPane(paneID entity.PaneID) *browserWindow {
495+ if paneID == "" || a.tabs == nil {
496+ return nil
497+ }
498+ for _, tab := range a.tabs.Tabs {
499+ if tab == nil || tab.Workspace == nil {
500+ continue
501+ }
502+ if tab.Workspace.FindPane(paneID) != nil {
503+ return a.browserWindowForTab(tab.ID)
504+ }
505+ }
506+ return nil
507+}
508+
509+func (a *App) activateBrowserWindow(bw *browserWindow) {
510+ if bw == nil {
511+ return
512+ }
513+ if bw.mainWindow != nil {
514+ a.mainWindow = bw.mainWindow
515+ }
516+ a.keyboardHandler = bw.keyboardHandler
517+ a.globalShortcutHandler = bw.globalShortcutHandler
518+ if a.tabCoord != nil && bw.mainWindow != nil {
519+ a.tabCoord.SetMainWindow(bw.mainWindow)
520+ }
521+ if a.tabs != nil && bw.activeTabID != "" && a.tabs.Find(bw.activeTabID) != nil {
522+ a.tabs.SetActive(bw.activeTabID)
523+ }
524+}
525+
490 func (a *App) setBrowserWindowForTab(tabID entity.TabID, bw *browserWindow) {526 func (a *App) setBrowserWindowForTab(tabID entity.TabID, bw *browserWindow) {
491 if bw == nil {527 if bw == nil {
492 return528 return
493 }529 }
494 a.ensureWindowForTabMap()530 a.ensureWindowForTabMap()
495 a.windowForTab[tabID] = bw531 a.windowForTab[tabID] = bw
532+ if bw.activeTabID == "" {
533+ bw.activeTabID = tabID
534+ }
496 }535 }
497 536
498 func (a *App) startBrowserLaunchRelayListener(ctx context.Context) {537 func (a *App) startBrowserLaunchRelayListener(ctx context.Context) {
@@ -525,18 +564,24 @@ func (a *App) openFreshWindow(ctx context.Context, url string) error {
525 a.registerBrowserWindow(created)564 a.registerBrowserWindow(created)
526 565
527 if a.tabs == nil || a.tabs.Count() == 0 {566 if a.tabs == nil || a.tabs.Count() == 0 {
567+ a.activateBrowserWindow(created)
528 return nil568 return nil
529 }569 }
530 570
571+ var openErr error
531 if a.tabCoord != nil {572 if a.tabCoord != nil {
532- return a.openFreshWindowWithTabCoord(ctx, url, created)573+ openErr = a.openFreshWindowWithTabCoord(ctx, url, created)
533- }574+ } else if a.tabsUC == nil {
534-575+ a.activateBrowserWindow(created)
535- if a.tabsUC == nil {
536 return nil576 return nil
577+ } else {
578+ openErr = a.openFreshWindowWithTabsUC(ctx, url, created)
537 }579 }
538-580+ if openErr != nil {
539- return a.openFreshWindowWithTabsUC(ctx, url, created)581+ return openErr
582+ }
583+ a.activateBrowserWindow(created)
584+ return nil
540 }585 }
541 586
542 func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error {587 func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, created *browserWindow) error {
@@ -579,6 +624,7 @@ func (a *App) openFreshWindowWithTabCoord(ctx context.Context, url string, creat
579 return fmt.Errorf("workspace view not created for tab %s", createdTab.ID)624 return fmt.Errorf("workspace view not created for tab %s", createdTab.ID)
580 }625 }
581 a.setBrowserWindowForTab(createdTab.ID, created)626 a.setBrowserWindowForTab(createdTab.ID, created)
627+ created.activeTabID = createdTab.ID
582 if created.mainWindow != nil {628 if created.mainWindow != nil {
583 created.mainWindow.Show()629 created.mainWindow.Show()
584 }630 }
@@ -591,9 +637,14 @@ func (a *App) openFreshWindowWithTabsUC(ctx context.Context, url string, created
591 InitialURL: url,637 InitialURL: url,
592 })638 })
593 if err != nil {639 if err != nil {
640+ a.removeBrowserWindow(created.id)
641+ if created.mainWindow != nil {
642+ created.mainWindow.Destroy()
643+ }
594 return err644 return err
595 }645 }
596 a.setBrowserWindowForTab(output.Tab.ID, created)646 a.setBrowserWindowForTab(output.Tab.ID, created)
647+ created.activeTabID = output.Tab.ID
597 if a.widgetFactory != nil {648 if a.widgetFactory != nil {
598 a.tabCreationWindow = created649 a.tabCreationWindow = created
599 defer func() {650 defer func() {
@@ -832,37 +883,39 @@ func (a *autoCopyConfigFn) IsAutoCopyEnabled() bool {
832 }883 }
833 884
834 func (a *App) initKeyboardHandler(ctx context.Context) {885 func (a *App) initKeyboardHandler(ctx context.Context) {
886+ bw := a.browserWindowForMainWindow(a.mainWindow)
887+ if bw == nil {
888+ return
889+ }
890+ a.initBrowserWindowInput(ctx, bw)
891+}
892+
893+func (a *App) initBrowserWindowInput(ctx context.Context, bw *browserWindow) {
835 log := logging.FromContext(ctx)894 log := logging.FromContext(ctx)
836 895
837- if a.mainWindow == nil || a.deps == nil || a.deps.Config == nil {896+ if bw == nil || bw.mainWindow == nil || a.deps == nil || a.deps.Config == nil || a.kbDispatcher == nil {
838 return897 return
839 }898 }
840 899
841- // Create keyboard handler and wire to dispatcher.900+ bw.keyboardHandler = input.NewKeyboardHandler(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)
842- a.keyboardHandler = input.NewKeyboardHandler(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)901+ bw.keyboardHandler.SetOnAction(func(actionCtx context.Context, action input.Action) error {
843- a.keyboardHandler.SetOnAction(func(ctx context.Context, action input.Action) error {902+ a.activateBrowserWindow(bw)
844 if action == input.ActionClosePane {903 if action == input.ActionClosePane {
845- if a.closeAndReleaseActiveFloatingPane(ctx) {904+ if a.closeAndReleaseActiveFloatingPane(actionCtx) {
846 return nil905 return nil
847 }906 }
848 }907 }
849- return a.kbDispatcher.Dispatch(ctx, action)908+ return a.kbDispatcher.Dispatch(actionCtx, action)
850 })909 })
851- a.keyboardHandler.SetOnEscape(func(ctx context.Context) bool {910+ bw.keyboardHandler.SetOnEscape(func(escapeCtx context.Context) bool {
852- return a.handleGlobalEscape(ctx)911+ a.activateBrowserWindow(bw)
912+ return a.handleGlobalEscape(escapeCtx)
853 })913 })
854- a.keyboardHandler.SetOnModeChange(func(from, to input.Mode) {914+ bw.keyboardHandler.SetOnModeChange(func(from, to input.Mode) {
915+ a.activateBrowserWindow(bw)
855 a.handleModeChange(ctx, from, to)916 a.handleModeChange(ctx, from, to)
856 })917 })
857- // Wire key routing policy.918+ bw.keyboardHandler.SetRouteKey(func(kc input.KeyContext) input.KeyRoute {
858- // Determines how each key event should be handled based on UI state:
859- // - Overlays visible (session manager, tab picker): pass all keys to widget
860- // - Omnibox visible: accent detection for text keys, shortcuts for modified keys
861- // - WebView focused: pass text/dead keys to WebKit IM for native compose,
862- // intercept shortcut-modified keys (Ctrl+, Alt+)
863- // - Default: handle through shortcut system
864- a.keyboardHandler.SetRouteKey(func(kc input.KeyContext) input.KeyRoute {
865- // Overlays take full keyboard control
866 if a.sessionManager != nil && a.sessionManager.IsVisible() {919 if a.sessionManager != nil && a.sessionManager.IsVisible() {
867 return input.RoutePassToWidget920 return input.RoutePassToWidget
868 }921 }
@@ -870,19 +923,8 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
870 return input.RoutePassToWidget923 return input.RoutePassToWidget
871 }924 }
872 925
873- wsView := a.activeWorkspaceView()
874- if wsView == nil {
875- return input.RouteHandleShortcuts
876- }
877-
878- // Check actual focus rather than visibility: find bar can remain
879- // visible after focus returns to the WebView, so visibility alone
880- // would misroute keys.
881 if a.accentFocusProvider != nil {926 if a.accentFocusProvider != nil {
882 if _, ok := a.accentFocusProvider.GetFocusedInput().(port.EntryInputTarget); ok {927 if _, ok := a.accentFocusProvider.GetFocusedInput().(port.EntryInputTarget); ok {
883- // GTK Entry focused (omnibox or find bar): Alt-modified keys go to
884- // shortcuts (pane navigation), other keys pass through to the widget's
885- // own capture-phase key controller (which handles accent detection)
886 if kc.Modifiers&input.ModAlt != 0 {928 if kc.Modifiers&input.ModAlt != 0 {
887 return input.RouteHandleShortcuts929 return input.RouteHandleShortcuts
888 }930 }
@@ -890,8 +932,6 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
890 }932 }
891 }933 }
892 934
893- // WebView focused: text/dead keys pass through for native IM compose,
894- // shortcut-modified keys (Ctrl+, Alt+) are handled by shortcut system
895 if input.IsShortcutModified(kc.Modifiers) {935 if input.IsShortcutModified(kc.Modifiers) {
896 return input.RouteHandleShortcuts936 return input.RouteHandleShortcuts
897 }937 }
@@ -899,31 +939,43 @@ func (a *App) initKeyboardHandler(ctx context.Context) {
899 return input.RoutePassToWidget939 return input.RoutePassToWidget
900 }940 }
901 941
902- // Non-text, non-shortcut keys (F-keys, arrows, etc.): shortcut system
903 return input.RouteHandleShortcuts942 return input.RouteHandleShortcuts
904 })943 })
905- // Wire accent handler for dead keys support
906 if a.insertAccentUC != nil {944 if a.insertAccentUC != nil {
907- a.keyboardHandler.SetAccentHandler(a.insertAccentUC)945+ bw.keyboardHandler.SetAccentHandler(a.insertAccentUC)
908 }946 }
909- a.keyboardHandler.AttachTo(a.mainWindow.Window())947+ bw.keyboardHandler.AttachTo(bw.mainWindow.Window())
910 948
911- // Create global shortcut handler for Alt+1-9 tab switching.949+ bw.globalShortcutHandler = input.NewGlobalShortcutHandler(
912- // This uses GtkShortcutController with GTK_SHORTCUT_SCOPE_GLOBAL
913- // to intercept shortcuts even when WebView has focus.
914- a.globalShortcutHandler = input.NewGlobalShortcutHandler(
915 ctx,950 ctx,
916- a.mainWindow.Window(),951+ bw.mainWindow.Window(),
917 &a.deps.Config.Workspace,952 &a.deps.Config.Workspace,
918 &a.deps.Config.Session,953 &a.deps.Config.Session,
919- a.keyboardHandler,954+ bw.keyboardHandler,
920- func(ctx context.Context, action input.Action) error {955+ func(actionCtx context.Context, action input.Action) error {
921- return a.kbDispatcher.Dispatch(ctx, action)956+ a.activateBrowserWindow(bw)
957+ return a.kbDispatcher.Dispatch(actionCtx, action)
922 },958 },
923 )959 )
924- if a.globalShortcutHandler == nil {960+ if bw.globalShortcutHandler == nil {
925- log.Warn().Msg("global shortcut handler creation failed, shortcuts may not work when WebView has focus")961+ log.Warn().Str("window_id", bw.id).Msg("global shortcut handler creation failed, shortcuts may not work when WebView has focus")
926 }962 }
963+
964+ a.activateBrowserWindow(bw)
965+}
966+
967+func (a *App) wireBrowserWindowTabBar(ctx context.Context, bw *browserWindow) {
968+ if bw == nil || bw.mainWindow == nil || bw.mainWindow.TabBar() == nil || a.tabCoord == nil {
969+ return
970+ }
971+ bw.mainWindow.TabBar().SetOnSwitch(func(tabID entity.TabID) {
972+ a.activateBrowserWindow(bw)
973+ if err := a.tabCoord.Switch(ctx, tabID); err != nil {
974+ logging.FromContext(ctx).Error().Err(err).Str("tab_id", string(tabID)).Str("window_id", bw.id).Msg("tab switch failed")
975+ return
976+ }
977+ bw.activeTabID = tabID
978+ })
927 }979 }
928 980
929 // handleAccentKeyPress handles accent key press events for GTK entry widgets981 // handleAccentKeyPress handles accent key press events for GTK entry widgets
@@ -1849,6 +1901,10 @@ func (a *App) initCoordinators(ctx context.Context) {
1849 a.createWorkspaceView(ctx, tab)1901 a.createWorkspaceView(ctx, tab)
1850 })1902 })
1851 a.tabCoord.SetOnTabSwitched(func(ctx context.Context, tab *entity.Tab) {1903 a.tabCoord.SetOnTabSwitched(func(ctx context.Context, tab *entity.Tab) {
1904+ if bw := a.browserWindowForTab(tab.ID); bw != nil {
1905+ bw.activeTabID = tab.ID
1906+ a.activateBrowserWindow(bw)
1907+ }
1852 a.switchWorkspaceView(ctx, tab.ID)1908 a.switchWorkspaceView(ctx, tab.ID)
1853 })1909 })
1854 a.tabCoord.SetOnQuit(a.Quit)1910 a.tabCoord.SetOnQuit(a.Quit)
@@ -1858,13 +1914,8 @@ func (a *App) initCoordinators(ctx context.Context) {
1858 a.attachPopupToTab(ctx, tabID, pane, wv)1914 a.attachPopupToTab(ctx, tabID, pane, wv)
1859 })1915 })
1860 1916
1861- // Wire tab bar click handling to coordinator1917+ for _, bw := range a.browserWindows {
1862- if a.mainWindow != nil && a.mainWindow.TabBar() != nil {1918+ a.wireBrowserWindowTabBar(ctx, bw)
1863- a.mainWindow.TabBar().SetOnSwitch(func(tabID entity.TabID) {
1864- if err := a.tabCoord.Switch(ctx, tabID); err != nil {
1865- log.Error().Err(err).Str("tab_id", string(tabID)).Msg("tab switch failed")
1866- }
1867- })
1868 }1919 }
1869 1920
1870 // Set fullscreen callback to hide/show tab bar (after tabCoord is initialized)1921 // Set fullscreen callback to hide/show tab bar (after tabCoord is initialized)
@@ -1999,6 +2050,9 @@ func (a *App) initCoordinators(ctx context.Context) {
1999 a.deps.Config.Workspace.NewPaneURL,2050 a.deps.Config.Workspace.NewPaneURL,
2000 )2051 )
2001 a.wireKeyboardActions()2052 a.wireKeyboardActions()
2053+ for _, bw := range a.browserWindows {
2054+ a.initBrowserWindowInput(ctx, bw)
2055+ }
2002 2056
2003 log.Debug().Msg("coordinators initialized")2057 log.Debug().Msg("coordinators initialized")
2004 }2058 }
@@ -2041,33 +2095,73 @@ func (a *App) wireKeyboardActions() {
2041 }2095 }
2042 2096
2043 func (a *App) wireWebRTCPermissionIndicator() {2097 func (a *App) wireWebRTCPermissionIndicator() {
2044- if a.webrtcIndicator == nil || a.contentCoord == nil {2098+ if a.contentCoord == nil {
2045 return2099 return
2046 }2100 }
2047 ctx := context.Background()2101 ctx := context.Background()
2048 if a.deps != nil && a.deps.Ctx != nil {2102 if a.deps != nil && a.deps.Ctx != nil {
2049 ctx = a.deps.Ctx2103 ctx = a.deps.Ctx
2050 }2104 }
2051- log := logging.FromContext(ctx)
2052 2105
2053- a.contentCoord.SetOnPermissionActivity(func(origin string, permTypes []entity.PermissionType, state content.PermissionActivityState) {2106+ a.contentCoord.SetOnPermissionActivity(func(
2054- a.webrtcIndicator.SetOrigin(origin)2107+ paneID entity.PaneID,
2108+ origin string,
2109+ permTypes []entity.PermissionType,
2110+ state content.PermissionActivityState,
2111+ ) {
2112+ bw := a.browserWindowForPane(paneID)
2113+ if bw == nil || bw.webrtcIndicator == nil {
2114+ return
2115+ }
2116+ if state == content.PermissionActivityRequesting && a.deps != nil && a.deps.PermissionUC != nil && bw.permissionDialog != nil {
2117+ a.deps.PermissionUC.SetDialogPresenter(bw.permissionDialog)
2118+ }
2119+ bw.webrtcIndicator.SetOrigin(origin)
2055 2120
2056 switch state {2121 switch state {
2057 case content.PermissionActivityRequesting:2122 case content.PermissionActivityRequesting:
2058- a.webrtcIndicator.MarkRequesting(permTypes)2123+ bw.webrtcIndicator.MarkRequesting(permTypes)
2059 case content.PermissionActivityAllowed:2124 case content.PermissionActivityAllowed:
2060- a.webrtcIndicator.MarkAllowed(permTypes)2125+ bw.webrtcIndicator.MarkAllowed(permTypes)
2061 case content.PermissionActivityBlocked:2126 case content.PermissionActivityBlocked:
2062- a.webrtcIndicator.MarkBlocked(permTypes)2127+ bw.webrtcIndicator.MarkBlocked(permTypes)
2063 }2128 }
2064 2129
2065 for _, permType := range permTypes {2130 for _, permType := range permTypes {
2066- a.syncWebRTCPermissionLockState(ctx, origin, permType)2131+ a.syncWebRTCPermissionLockState(ctx, bw.webrtcIndicator, origin, permType)
2067 }2132 }
2068 })2133 })
2069 2134
2070- a.webrtcIndicator.SetOnToggleLock(func(origin string, permType entity.PermissionType, state string, hasStored bool) {2135+ // Reset the owning window's indicator when that pane navigates away.
2136+ a.contentCoord.SetOnActiveNavigationCommitted(func(paneID entity.PaneID, uri string) {
2137+ bw := a.browserWindowForPane(paneID)
2138+ if bw == nil || bw.webrtcIndicator == nil {
2139+ return
2140+ }
2141+ newOrigin, err := urlutil.ExtractOrigin(uri)
2142+ if err != nil {
2143+ bw.webrtcIndicator.Reset()
2144+ return
2145+ }
2146+
2147+ currentOrigin := bw.webrtcIndicator.Origin()
2148+ if currentOrigin != "" && currentOrigin != newOrigin {
2149+ bw.webrtcIndicator.Reset()
2150+ }
2151+ })
2152+}
2153+
2154+func (a *App) wireBrowserWindowPermissionIndicator(bw *browserWindow) {
2155+ if bw == nil || bw.webrtcIndicator == nil {
2156+ return
2157+ }
2158+ ctx := context.Background()
2159+ if a.deps != nil && a.deps.Ctx != nil {
2160+ ctx = a.deps.Ctx
2161+ }
2162+ log := logging.FromContext(ctx)
2163+
2164+ bw.webrtcIndicator.SetOnToggleLock(func(origin string, permType entity.PermissionType, state string, hasStored bool) {
2071 if a.deps == nil || a.deps.PermissionUC == nil || origin == "" {2165 if a.deps == nil || a.deps.PermissionUC == nil || origin == "" {
2072 return2166 return
2073 }2167 }
@@ -2092,27 +2186,17 @@ func (a *App) wireWebRTCPermissionIndicator() {
2092 }2186 }
2093 }2187 }
2094 2188
2095- a.syncWebRTCPermissionLockState(ctx, origin, permType)2189+ a.syncWebRTCPermissionLockState(ctx, bw.webrtcIndicator, origin, permType)
2096- })
2097-
2098- // Reset indicator when the active pane navigates away from the current origin.
2099- a.contentCoord.SetOnActiveNavigationCommitted(func(uri string) {
2100- newOrigin, err := urlutil.ExtractOrigin(uri)
2101- if err != nil {
2102- // Internal pages (dumb://, about:) — clear the indicator.
2103- a.webrtcIndicator.Reset()
2104- return
2105- }
2106-
2107- currentOrigin := a.webrtcIndicator.Origin()
2108- if currentOrigin != "" && currentOrigin != newOrigin {
2109- a.webrtcIndicator.Reset()
2110- }
2111 })2190 })
2112 }2191 }
2113 2192
2114-func (a *App) syncWebRTCPermissionLockState(ctx context.Context, origin string, permType entity.PermissionType) {2193+func (a *App) syncWebRTCPermissionLockState(
2115- if a.webrtcIndicator == nil || a.deps == nil || a.deps.PermissionUC == nil || origin == "" {2194+ ctx context.Context,
2195+ indicator *component.WebRTCPermissionIndicator,
2196+ origin string,
2197+ permType entity.PermissionType,
2198+) {
2199+ if indicator == nil || a.deps == nil || a.deps.PermissionUC == nil || origin == "" {
2116 return2200 return
2117 }2201 }
2118 2202
@@ -2127,11 +2211,11 @@ func (a *App) syncWebRTCPermissionLockState(ctx context.Context, origin string,
2127 }2211 }
2128 2212
2129 if record == nil {2213 if record == nil {
2130- a.webrtcIndicator.SetStoredDecision(permType, entity.PermissionPrompt, false)2214+ indicator.SetStoredDecision(permType, entity.PermissionPrompt, false)
2131 return2215 return
2132 }2216 }
2133 2217
2134- a.webrtcIndicator.SetStoredDecision(permType, record.Decision, true)2218+ indicator.SetStoredDecision(permType, record.Decision, true)
2135 }2219 }
2136 2220
2137 // generateWindowID generates a unique ID for top-level browser windows.2221 // generateWindowID generates a unique ID for top-level browser windows.
@@ -2535,6 +2619,7 @@ func (a *App) switchWorkspaceView(ctx context.Context, tabID entity.TabID) {
2535 2619
2536 // Swap content (MainWindow.SetContent now properly removes old content)2620 // Swap content (MainWindow.SetContent now properly removes old content)
2537 if target := a.browserWindowForTab(tabID); target != nil && target.mainWindow != nil {2621 if target := a.browserWindowForTab(tabID); target != nil && target.mainWindow != nil {
2622+ target.activeTabID = tabID
2538 target.mainWindow.SetContent(gtkWidget)2623 target.mainWindow.SetContent(gtkWidget)
2539 } else if a.mainWindow != nil {2624 } else if a.mainWindow != nil {
2540 a.mainWindow.SetContent(gtkWidget)2625 a.mainWindow.SetContent(gtkWidget)
@@ -3411,13 +3496,16 @@ func (a *App) initConfigWatcher(ctx context.Context) {
3411 a.deps.OnConfigChange(func() {3496 a.deps.OnConfigChange(func() {
3412 cb := glib.SourceFunc(func(_ uintptr) bool {3497 cb := glib.SourceFunc(func(_ uintptr) bool {
3413 a.applyAppearanceConfig(ctx)3498 a.applyAppearanceConfig(ctx)
3414- // Reload keyboard shortcuts3499+ for _, bw := range a.browserWindows {
3415- if a.keyboardHandler != nil {3500+ if bw == nil {
3416- a.keyboardHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)3501+ continue
3417- }3502+ }
3418- // Reload global shortcuts (removes stale, re-registers from config)3503+ if bw.keyboardHandler != nil {
3419- if a.globalShortcutHandler != nil {3504+ bw.keyboardHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)
3420- a.globalShortcutHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)3505+ }
3506+ if bw.globalShortcutHandler != nil {
3507+ bw.globalShortcutHandler.ReloadShortcuts(ctx, &a.deps.Config.Workspace, &a.deps.Config.Session)
3508+ }
3421 }3509 }
3422 return false3510 return false
3423 })3511 })
M internal/ui/app_browser_launch_test.go +63 -0
@@ -12,6 +12,7 @@ import (
1212 "github.com/bnema/dumber/internal/domain/entity"
1313 "github.com/bnema/dumber/internal/ui/component"
1414 "github.com/bnema/dumber/internal/ui/coordinator"
15+ "github.com/bnema/dumber/internal/ui/input"
1516 "github.com/bnema/dumber/internal/ui/window"
1617 "github.com/bnema/puregotk/v4/gio"
1718 "github.com/bnema/puregotk/v4/gtk"
@@ -254,3 +255,65 @@ func TestApp_OpenFreshWindowTargetsNewWindowTabBar(t *testing.T) {
254255 t.Fatalf("new window tab bar visible = %v, want false", got)
255256 }
256257 }
258+
259+func TestApp_ActivateBrowserWindowSwitchesActiveWorkspace(t *testing.T) {
260+ tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))
261+ tab2 := entity.NewTab(entity.TabID("tab-2"), entity.WorkspaceID("workspace-2"), entity.NewPane(entity.PaneID("pane-2")))
262+ tabs := entity.NewTabList()
263+ tabs.Add(tab1)
264+ tabs.Add(tab2)
265+ ws1 := &component.WorkspaceView{}
266+ ws2 := &component.WorkspaceView{}
267+ first := &browserWindow{id: "window-1", activeTabID: tab1.ID, mainWindow: &window.MainWindow{}}
268+ second := &browserWindow{id: "window-2", activeTabID: tab2.ID, mainWindow: &window.MainWindow{}}
269+ kh := &input.KeyboardHandler{}
270+ gs := &input.GlobalShortcutHandler{}
271+ second.keyboardHandler = kh
272+ second.globalShortcutHandler = gs
273+ app := &App{
274+ tabs: tabs,
275+ mainWindow: first.mainWindow,
276+ browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
277+ windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second},
278+ workspaceViews: map[entity.TabID]*component.WorkspaceView{tab1.ID: ws1, tab2.ID: ws2},
279+ }
280+
281+ app.activateBrowserWindow(second)
282+
283+ if app.mainWindow != second.mainWindow {
284+ t.Fatalf("mainWindow = %p, want %p", app.mainWindow, second.mainWindow)
285+ }
286+ if app.tabs.ActiveTabID != tab2.ID {
287+ t.Fatalf("active tab = %q, want %q", app.tabs.ActiveTabID, tab2.ID)
288+ }
289+ if app.activeWorkspaceView() != ws2 {
290+ t.Fatalf("active workspace view = %p, want %p", app.activeWorkspaceView(), ws2)
291+ }
292+ if app.keyboardHandler != kh {
293+ t.Fatalf("keyboardHandler = %p, want %p", app.keyboardHandler, kh)
294+ }
295+ if app.globalShortcutHandler != gs {
296+ t.Fatalf("globalShortcutHandler = %p, want %p", app.globalShortcutHandler, gs)
297+ }
298+}
299+
300+func TestApp_BrowserWindowForPaneFindsOwningWindow(t *testing.T) {
301+ tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))
302+ tab2 := entity.NewTab(entity.TabID("tab-2"), entity.WorkspaceID("workspace-2"), entity.NewPane(entity.PaneID("pane-2")))
303+ tabs := entity.NewTabList()
304+ tabs.Add(tab1)
305+ tabs.Add(tab2)
306+ first := &browserWindow{id: "window-1"}
307+ second := &browserWindow{id: "window-2"}
308+ app := &App{
309+ tabs: tabs,
310+ browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
311+ windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second},
312+ }
313+
314+ got := app.browserWindowForPane(entity.PaneID("pane-2"))
315+
316+ if got != second {
317+ t.Fatalf("browserWindowForPane = %p, want %p", got, second)
318+ }
319+}
@@ -12,6 +12,7 @@ import (
12 "github.com/bnema/dumber/internal/domain/entity"12 "github.com/bnema/dumber/internal/domain/entity"
13 "github.com/bnema/dumber/internal/ui/component"13 "github.com/bnema/dumber/internal/ui/component"
14 "github.com/bnema/dumber/internal/ui/coordinator"14 "github.com/bnema/dumber/internal/ui/coordinator"
15+ "github.com/bnema/dumber/internal/ui/input"
15 "github.com/bnema/dumber/internal/ui/window"16 "github.com/bnema/dumber/internal/ui/window"
16 "github.com/bnema/puregotk/v4/gio"17 "github.com/bnema/puregotk/v4/gio"
17 "github.com/bnema/puregotk/v4/gtk"18 "github.com/bnema/puregotk/v4/gtk"
@@ -254,3 +255,65 @@ func TestApp_OpenFreshWindowTargetsNewWindowTabBar(t *testing.T) {
254 t.Fatalf("new window tab bar visible = %v, want false", got)255 t.Fatalf("new window tab bar visible = %v, want false", got)
255 }256 }
256 }257 }
258+
259+func TestApp_ActivateBrowserWindowSwitchesActiveWorkspace(t *testing.T) {
260+ tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))
261+ tab2 := entity.NewTab(entity.TabID("tab-2"), entity.WorkspaceID("workspace-2"), entity.NewPane(entity.PaneID("pane-2")))
262+ tabs := entity.NewTabList()
263+ tabs.Add(tab1)
264+ tabs.Add(tab2)
265+ ws1 := &component.WorkspaceView{}
266+ ws2 := &component.WorkspaceView{}
267+ first := &browserWindow{id: "window-1", activeTabID: tab1.ID, mainWindow: &window.MainWindow{}}
268+ second := &browserWindow{id: "window-2", activeTabID: tab2.ID, mainWindow: &window.MainWindow{}}
269+ kh := &input.KeyboardHandler{}
270+ gs := &input.GlobalShortcutHandler{}
271+ second.keyboardHandler = kh
272+ second.globalShortcutHandler = gs
273+ app := &App{
274+ tabs: tabs,
275+ mainWindow: first.mainWindow,
276+ browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
277+ windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second},
278+ workspaceViews: map[entity.TabID]*component.WorkspaceView{tab1.ID: ws1, tab2.ID: ws2},
279+ }
280+
281+ app.activateBrowserWindow(second)
282+
283+ if app.mainWindow != second.mainWindow {
284+ t.Fatalf("mainWindow = %p, want %p", app.mainWindow, second.mainWindow)
285+ }
286+ if app.tabs.ActiveTabID != tab2.ID {
287+ t.Fatalf("active tab = %q, want %q", app.tabs.ActiveTabID, tab2.ID)
288+ }
289+ if app.activeWorkspaceView() != ws2 {
290+ t.Fatalf("active workspace view = %p, want %p", app.activeWorkspaceView(), ws2)
291+ }
292+ if app.keyboardHandler != kh {
293+ t.Fatalf("keyboardHandler = %p, want %p", app.keyboardHandler, kh)
294+ }
295+ if app.globalShortcutHandler != gs {
296+ t.Fatalf("globalShortcutHandler = %p, want %p", app.globalShortcutHandler, gs)
297+ }
298+}
299+
300+func TestApp_BrowserWindowForPaneFindsOwningWindow(t *testing.T) {
301+ tab1 := entity.NewTab(entity.TabID("tab-1"), entity.WorkspaceID("workspace-1"), entity.NewPane(entity.PaneID("pane-1")))
302+ tab2 := entity.NewTab(entity.TabID("tab-2"), entity.WorkspaceID("workspace-2"), entity.NewPane(entity.PaneID("pane-2")))
303+ tabs := entity.NewTabList()
304+ tabs.Add(tab1)
305+ tabs.Add(tab2)
306+ first := &browserWindow{id: "window-1"}
307+ second := &browserWindow{id: "window-2"}
308+ app := &App{
309+ tabs: tabs,
310+ browserWindows: map[string]*browserWindow{first.id: first, second.id: second},
311+ windowForTab: map[entity.TabID]*browserWindow{tab1.ID: first, tab2.ID: second},
312+ }
313+
314+ got := app.browserWindowForPane(entity.PaneID("pane-2"))
315+
316+ if got != second {
317+ t.Fatalf("browserWindowForPane = %p, want %p", got, second)
318+ }
319+}
M internal/ui/browser_window.go +15 -7
@@ -3,13 +3,22 @@ package ui
33 import (
44 "context"
55
6+ "github.com/bnema/dumber/internal/application/port"
7+ "github.com/bnema/dumber/internal/domain/entity"
8+ "github.com/bnema/dumber/internal/ui/component"
9+ "github.com/bnema/dumber/internal/ui/input"
610 "github.com/bnema/dumber/internal/ui/window"
711 )
812
913 type browserWindow struct {
10- id string
11- initialURL string
12- mainWindow *window.MainWindow
14+ id string
15+ initialURL string
16+ activeTabID entity.TabID
17+ mainWindow *window.MainWindow
18+ keyboardHandler *input.KeyboardHandler
19+ globalShortcutHandler *input.GlobalShortcutHandler
20+ permissionDialog port.PermissionDialogPresenter
21+ webrtcIndicator *component.WebRTCPermissionIndicator
1322 }
1423
1524 func (a *App) registerBrowserWindow(bw *browserWindow) {
@@ -35,14 +44,13 @@ func (a *App) removeBrowserWindow(id string) {
3544 delete(a.browserWindows, id)
3645 for _, remaining := range a.browserWindows {
3746 if remaining != nil && remaining.mainWindow != nil {
38- a.mainWindow = remaining.mainWindow
39- if a.tabCoord != nil {
40- a.tabCoord.SetMainWindow(remaining.mainWindow)
41- }
47+ a.activateBrowserWindow(remaining)
4248 return
4349 }
4450 }
4551 a.mainWindow = nil
52+ a.keyboardHandler = nil
53+ a.globalShortcutHandler = nil
4654 if a.tabCoord != nil {
4755 a.tabCoord.SetMainWindow(nil)
4856 }
@@ -3,13 +3,22 @@ package ui
3 import (3 import (
4 "context"4 "context"
5 5
6+ "github.com/bnema/dumber/internal/application/port"
7+ "github.com/bnema/dumber/internal/domain/entity"
8+ "github.com/bnema/dumber/internal/ui/component"
9+ "github.com/bnema/dumber/internal/ui/input"
6 "github.com/bnema/dumber/internal/ui/window"10 "github.com/bnema/dumber/internal/ui/window"
7 )11 )
8 12
9 type browserWindow struct {13 type browserWindow struct {
10- id string14+ id string
11- initialURL string15+ initialURL string
12- mainWindow *window.MainWindow16+ activeTabID entity.TabID
17+ mainWindow *window.MainWindow
18+ keyboardHandler *input.KeyboardHandler
19+ globalShortcutHandler *input.GlobalShortcutHandler
20+ permissionDialog port.PermissionDialogPresenter
21+ webrtcIndicator *component.WebRTCPermissionIndicator
13 }22 }
14 23
15 func (a *App) registerBrowserWindow(bw *browserWindow) {24 func (a *App) registerBrowserWindow(bw *browserWindow) {
@@ -35,14 +44,13 @@ func (a *App) removeBrowserWindow(id string) {
35 delete(a.browserWindows, id)44 delete(a.browserWindows, id)
36 for _, remaining := range a.browserWindows {45 for _, remaining := range a.browserWindows {
37 if remaining != nil && remaining.mainWindow != nil {46 if remaining != nil && remaining.mainWindow != nil {
38- a.mainWindow = remaining.mainWindow47+ a.activateBrowserWindow(remaining)
39- if a.tabCoord != nil {
40- a.tabCoord.SetMainWindow(remaining.mainWindow)
41- }
42 return48 return
43 }49 }
44 }50 }
45 a.mainWindow = nil51 a.mainWindow = nil
52+ a.keyboardHandler = nil
53+ a.globalShortcutHandler = nil
46 if a.tabCoord != nil {54 if a.tabCoord != nil {
47 a.tabCoord.SetMainWindow(nil)55 a.tabCoord.SetMainWindow(nil)
48 }56 }
M internal/ui/browser_window_test.go +19 -0
@@ -2,6 +2,7 @@ package ui
22
33 import (
44 "context"
5+ "errors"
56 "testing"
67
78 "github.com/bnema/dumber/internal/ui/window"
@@ -109,3 +110,21 @@ func TestOpenFreshWindow_DispatchesAndTracksBrowserWindow(t *testing.T) {
109110 t.Fatalf("browser window initialURL = %q, want %q", app.browserWindows["window-1"].initialURL, "https://example.com")
110111 }
111112 }
113+
114+func TestOpenFreshWindow_PropagatesFactoryError(t *testing.T) {
115+ app := &App{browserWindows: make(map[string]*browserWindow)}
116+ app.dispatchOnMainThread = func(fn func()) { fn() }
117+ wantErr := errors.New("factory error")
118+ app.browserWindowFactory = func(context.Context, string) (*browserWindow, error) {
119+ return nil, wantErr
120+ }
121+
122+ err := app.OpenFreshWindow(context.Background(), "https://example.com")
123+
124+ if !errors.Is(err, wantErr) {
125+ t.Fatalf("expected error %v, got %v", wantErr, err)
126+ }
127+ if got := len(app.browserWindows); got != 0 {
128+ t.Fatalf("browserWindows length = %d, want 0", got)
129+ }
130+}
@@ -2,6 +2,7 @@ package ui
2 2
3 import (3 import (
4 "context"4 "context"
5+ "errors"
5 "testing"6 "testing"
6 7
7 "github.com/bnema/dumber/internal/ui/window"8 "github.com/bnema/dumber/internal/ui/window"
@@ -109,3 +110,21 @@ func TestOpenFreshWindow_DispatchesAndTracksBrowserWindow(t *testing.T) {
109 t.Fatalf("browser window initialURL = %q, want %q", app.browserWindows["window-1"].initialURL, "https://example.com")110 t.Fatalf("browser window initialURL = %q, want %q", app.browserWindows["window-1"].initialURL, "https://example.com")
110 }111 }
111 }112 }
113+
114+func TestOpenFreshWindow_PropagatesFactoryError(t *testing.T) {
115+ app := &App{browserWindows: make(map[string]*browserWindow)}
116+ app.dispatchOnMainThread = func(fn func()) { fn() }
117+ wantErr := errors.New("factory error")
118+ app.browserWindowFactory = func(context.Context, string) (*browserWindow, error) {
119+ return nil, wantErr
120+ }
121+
122+ err := app.OpenFreshWindow(context.Background(), "https://example.com")
123+
124+ if !errors.Is(err, wantErr) {
125+ t.Fatalf("expected error %v, got %v", wantErr, err)
126+ }
127+ if got := len(app.browserWindows); got != 0 {
128+ t.Fatalf("browserWindows length = %d, want 0", got)
129+ }
130+}
M internal/ui/coordinator/content/callbacks.go +3 -2
@@ -118,7 +118,7 @@ func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.P
118118 }
119119 },
120120 OnPermissionRequest: func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool {
121- return c.handlePermissionRequest(ctx, origin, permTypes, metadata, allow, deny)
121+ return c.handlePermissionRequest(ctx, paneID, origin, permTypes, metadata, allow, deny)
122122 },
123123 }
124124
@@ -189,6 +189,7 @@ func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.P
189189 // It delegates to the permission use case which handles auto-allow, stored permissions, and dialogs.
190190 func (c *Coordinator) handlePermissionRequest(
191191 ctx context.Context,
192+ paneID entity.PaneID,
192193 origin string,
193194 permTypes []string,
194195 metadata map[string]string,
@@ -226,7 +227,7 @@ func (c *Coordinator) handlePermissionRequest(
226227 if c.onPermissionActivity == nil || len(trackedTypes) == 0 {
227228 return
228229 }
229- c.onPermissionActivity(origin, trackedTypes, state)
230+ c.onPermissionActivity(paneID, origin, trackedTypes, state)
230231 }
231232
232233 notifyActivity(PermissionActivityRequesting)
@@ -118,7 +118,7 @@ func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.P
118 }118 }
119 },119 },
120 OnPermissionRequest: func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool {120 OnPermissionRequest: func(origin string, permTypes []string, metadata map[string]string, allow, deny func()) bool {
121- return c.handlePermissionRequest(ctx, origin, permTypes, metadata, allow, deny)121+ return c.handlePermissionRequest(ctx, paneID, origin, permTypes, metadata, allow, deny)
122 },122 },
123 }123 }
124 124
@@ -189,6 +189,7 @@ func (c *Coordinator) setupWebViewCallbacks(ctx context.Context, paneID entity.P
189 // It delegates to the permission use case which handles auto-allow, stored permissions, and dialogs.189 // It delegates to the permission use case which handles auto-allow, stored permissions, and dialogs.
190 func (c *Coordinator) handlePermissionRequest(190 func (c *Coordinator) handlePermissionRequest(
191 ctx context.Context,191 ctx context.Context,
192+ paneID entity.PaneID,
192 origin string,193 origin string,
193 permTypes []string,194 permTypes []string,
194 metadata map[string]string,195 metadata map[string]string,
@@ -226,7 +227,7 @@ func (c *Coordinator) handlePermissionRequest(
226 if c.onPermissionActivity == nil || len(trackedTypes) == 0 {227 if c.onPermissionActivity == nil || len(trackedTypes) == 0 {
227 return228 return
228 }229 }
229- c.onPermissionActivity(origin, trackedTypes, state)230+ c.onPermissionActivity(paneID, origin, trackedTypes, state)
230 }231 }
231 232
232 notifyActivity(PermissionActivityRequesting)233 notifyActivity(PermissionActivityRequesting)
M internal/ui/coordinator/content/coordinator.go +4 -4
@@ -59,10 +59,10 @@ type Coordinator struct {
5959 onWindowTitleChanged func(title string)
6060
6161 // Callback when media permission activity changes (requesting/allowed/blocked).
62- onPermissionActivity func(origin string, permTypes []entity.PermissionType, state PermissionActivityState)
62+ onPermissionActivity func(paneID entity.PaneID, origin string, permTypes []entity.PermissionType, state PermissionActivityState)
6363
6464 // Callback when the active pane commits a navigation (new page loading).
65- onActiveNavigationCommitted func(uri string)
65+ onActiveNavigationCommitted func(paneID entity.PaneID, uri string)
6666
6767 // Callback when the WebView becomes visible (first real commit)
6868 onWebViewShown func(paneID entity.PaneID)
@@ -187,13 +187,13 @@ func (c *Coordinator) SetOnHistoryRecord(fn func(ctx context.Context, paneID ent
187187
188188 // SetOnPermissionActivity sets a callback for WebRTC permission activity changes.
189189 func (c *Coordinator) SetOnPermissionActivity(
190- fn func(origin string, permTypes []entity.PermissionType, state PermissionActivityState),
190+ fn func(paneID entity.PaneID, origin string, permTypes []entity.PermissionType, state PermissionActivityState),
191191 ) {
192192 c.onPermissionActivity = fn
193193 }
194194
195195 // SetOnActiveNavigationCommitted sets a callback fired when the active pane commits a navigation.
196-func (c *Coordinator) SetOnActiveNavigationCommitted(fn func(uri string)) {
196+func (c *Coordinator) SetOnActiveNavigationCommitted(fn func(paneID entity.PaneID, uri string)) {
197197 c.onActiveNavigationCommitted = fn
198198 }
199199
@@ -59,10 +59,10 @@ type Coordinator struct {
59 onWindowTitleChanged func(title string)59 onWindowTitleChanged func(title string)
60 60
61 // Callback when media permission activity changes (requesting/allowed/blocked).61 // Callback when media permission activity changes (requesting/allowed/blocked).
62- onPermissionActivity func(origin string, permTypes []entity.PermissionType, state PermissionActivityState)62+ onPermissionActivity func(paneID entity.PaneID, origin string, permTypes []entity.PermissionType, state PermissionActivityState)
63 63
64 // Callback when the active pane commits a navigation (new page loading).64 // Callback when the active pane commits a navigation (new page loading).
65- onActiveNavigationCommitted func(uri string)65+ onActiveNavigationCommitted func(paneID entity.PaneID, uri string)
66 66
67 // Callback when the WebView becomes visible (first real commit)67 // Callback when the WebView becomes visible (first real commit)
68 onWebViewShown func(paneID entity.PaneID)68 onWebViewShown func(paneID entity.PaneID)
@@ -187,13 +187,13 @@ func (c *Coordinator) SetOnHistoryRecord(fn func(ctx context.Context, paneID ent
187 187
188 // SetOnPermissionActivity sets a callback for WebRTC permission activity changes.188 // SetOnPermissionActivity sets a callback for WebRTC permission activity changes.
189 func (c *Coordinator) SetOnPermissionActivity(189 func (c *Coordinator) SetOnPermissionActivity(
190- fn func(origin string, permTypes []entity.PermissionType, state PermissionActivityState),190+ fn func(paneID entity.PaneID, origin string, permTypes []entity.PermissionType, state PermissionActivityState),
191 ) {191 ) {
192 c.onPermissionActivity = fn192 c.onPermissionActivity = fn
193 }193 }
194 194
195 // SetOnActiveNavigationCommitted sets a callback fired when the active pane commits a navigation.195 // SetOnActiveNavigationCommitted sets a callback fired when the active pane commits a navigation.
196-func (c *Coordinator) SetOnActiveNavigationCommitted(fn func(uri string)) {196+func (c *Coordinator) SetOnActiveNavigationCommitted(fn func(paneID entity.PaneID, uri string)) {
197 c.onActiveNavigationCommitted = fn197 c.onActiveNavigationCommitted = fn
198 }198 }
199 199
M internal/ui/coordinator/content/navigation.go +2 -2
@@ -123,13 +123,13 @@ func (c *Coordinator) notifyActiveNavigation(paneID entity.PaneID, uri string) {
123123 c.activePaneOverrideMu.RUnlock()
124124 if override != "" {
125125 if override == paneID {
126- c.onActiveNavigationCommitted(uri)
126+ c.onActiveNavigationCommitted(paneID, uri)
127127 }
128128 return
129129 }
130130 ws, _ := c.getActiveWS()
131131 if ws != nil && ws.ActivePaneID == paneID {
132- c.onActiveNavigationCommitted(uri)
132+ c.onActiveNavigationCommitted(paneID, uri)
133133 }
134134 }
135135
@@ -123,13 +123,13 @@ func (c *Coordinator) notifyActiveNavigation(paneID entity.PaneID, uri string) {
123 c.activePaneOverrideMu.RUnlock()123 c.activePaneOverrideMu.RUnlock()
124 if override != "" {124 if override != "" {
125 if override == paneID {125 if override == paneID {
126- c.onActiveNavigationCommitted(uri)126+ c.onActiveNavigationCommitted(paneID, uri)
127 }127 }
128 return128 return
129 }129 }
130 ws, _ := c.getActiveWS()130 ws, _ := c.getActiveWS()
131 if ws != nil && ws.ActivePaneID == paneID {131 if ws != nil && ws.ActivePaneID == paneID {
132- c.onActiveNavigationCommitted(uri)132+ c.onActiveNavigationCommitted(paneID, uri)
133 }133 }
134 }134 }
135 135

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