detached 31b1bbf

fix(cef): avoid overwriting resumed oauth parent state

commit
31b1bbf5470337deeef1c7d072359156cbe294a2
author
brice · · 2026-04-21 15:02 UTC
parent
bc78f4e
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +52 -47
M internal/ui/coordinator/content/coordinator.go +6 -5
@@ -125,11 +125,12 @@ type pendingThemeUpdate struct {
125125 }
126126
127127 type popupOAuthState struct {
128- ParentPaneID entity.PaneID
129- CallbackURI string
130- Success bool
131- Error bool
132- Seen bool
128+ ParentPaneID entity.PaneID
129+ ParentURIAtOpen string
130+ CallbackURI string
131+ Success bool
132+ Error bool
133+ Seen bool
133134 }
134135
135136 // PermissionActivityState represents the visible state for media permission activity.
@@ -125,11 +125,12 @@ type pendingThemeUpdate struct {
125 }125 }
126 126
127 type popupOAuthState struct {127 type popupOAuthState struct {
128- ParentPaneID entity.PaneID128+ ParentPaneID entity.PaneID
129- CallbackURI string129+ ParentURIAtOpen string
130- Success bool130+ CallbackURI string
131- Error bool131+ Success bool
132- Seen bool132+ Error bool
133+ Seen bool
133 }134 }
134 135
135 // PermissionActivityState represents the visible state for media permission activity.136 // PermissionActivityState represents the visible state for media permission activity.
M internal/ui/coordinator/content/oauth.go +13 -31
@@ -9,7 +9,6 @@ import (
99
1010 "github.com/bnema/dumber/internal/application/port"
1111 "github.com/bnema/dumber/internal/domain/entity"
12- urlutil "github.com/bnema/dumber/internal/domain/url"
1312 "github.com/bnema/dumber/internal/logging"
1413 "github.com/bnema/puregotk/v4/glib"
1514 )
@@ -324,14 +323,15 @@ func (c *Coordinator) setupOAuthAutoClose(
324323 })
325324 }
326325
327-func (c *Coordinator) trackOAuthPopup(popupID port.WebViewID, parentPaneID entity.PaneID) {
326+func (c *Coordinator) trackOAuthPopup(popupID port.WebViewID, parentPaneID entity.PaneID, parentURIAtOpen string) {
328327 c.popupMu.Lock()
329328 defer c.popupMu.Unlock()
330329 if c.popupOAuth == nil {
331330 c.popupOAuth = make(map[port.WebViewID]*popupOAuthState)
332331 }
333332 c.popupOAuth[popupID] = &popupOAuthState{
334- ParentPaneID: parentPaneID,
333+ ParentPaneID: parentPaneID,
334+ ParentURIAtOpen: strings.TrimSpace(parentURIAtOpen),
335335 }
336336 }
337337
@@ -350,15 +350,6 @@ func (c *Coordinator) capturePopupOAuthState(popupID port.WebViewID, uri string)
350350 state.Error = IsOAuthError(uri)
351351 }
352352
353-func shouldLoadOAuthCallbackInParent(parentURI, callbackURI string) bool {
354- if callbackURI == "" || !IsOAuthCallback(callbackURI) {
355- return false
356- }
357- parentDomain := urlutil.ExtractDomain(parentURI)
358- callbackDomain := urlutil.ExtractDomain(callbackURI)
359- return parentDomain != "" && parentDomain == callbackDomain
360-}
361-
362353 func (c *Coordinator) handlePopupOAuthClose(ctx context.Context, popupID port.WebViewID) {
363354 log := logging.FromContext(ctx)
364355
@@ -384,21 +375,18 @@ func (c *Coordinator) handlePopupOAuthClose(ctx context.Context, popupID port.We
384375 return
385376 }
386377
387- if strings.TrimSpace(state.CallbackURI) != "" {
388- c.scheduleParentPaneOAuthResume(ctx, state.ParentPaneID, popupID, state.CallbackURI)
389- return
390- }
391378 if !state.Success {
392379 return
393380 }
394381
395- c.scheduleParentPaneOAuthResume(ctx, state.ParentPaneID, popupID, "")
382+ c.scheduleParentPaneOAuthResume(ctx, state.ParentPaneID, popupID, state.ParentURIAtOpen, state.CallbackURI)
396383 }
397384
398385 func (c *Coordinator) scheduleParentPaneOAuthResume(
399386 ctx context.Context,
400387 parentPaneID entity.PaneID,
401388 popupID port.WebViewID,
389+ parentURIAtOpen string,
402390 callbackURI string,
403391 ) {
404392 c.popupMu.Lock()
@@ -413,7 +401,7 @@ func (c *Coordinator) scheduleParentPaneOAuthResume(
413401 delete(c.popupRefresh, parentPaneID)
414402 c.popupMu.Unlock()
415403 cb := glib.SourceFunc(func(_ uintptr) bool {
416- c.resumeParentPaneAfterOAuth(ctx, parentPaneID, popupID, callbackURI)
404+ c.resumeParentPaneAfterOAuth(ctx, parentPaneID, popupID, parentURIAtOpen, callbackURI)
417405 return false
418406 })
419407 glib.IdleAdd(&cb, 0)
@@ -425,6 +413,7 @@ func (c *Coordinator) resumeParentPaneAfterOAuth(
425413 ctx context.Context,
426414 parentPaneID entity.PaneID,
427415 popupID port.WebViewID,
416+ parentURIAtOpen string,
428417 callbackURI string,
429418 ) {
430419 log := logging.FromContext(ctx)
@@ -437,23 +426,15 @@ func (c *Coordinator) resumeParentPaneAfterOAuth(
437426 return
438427 }
439428
440- parentURI := wv.URI()
441- if shouldLoadOAuthCallbackInParent(parentURI, callbackURI) {
442- if err := wv.LoadURI(ctx, callbackURI); err != nil {
443- log.Warn().
444- Err(err).
445- Str("parent_pane_id", string(parentPaneID)).
446- Uint64("popup_id", uint64(popupID)).
447- Str("callback_uri", logging.TruncateURL(callbackURI, logURLMaxLen)).
448- Msg("failed to load oauth callback URI in parent pane")
449- return
450- }
451-
429+ parentURI := strings.TrimSpace(wv.URI())
430+ if parentURIAtOpen != "" && parentURI != "" && parentURI != parentURIAtOpen {
452431 log.Info().
453432 Str("parent_pane_id", string(parentPaneID)).
454433 Uint64("popup_id", uint64(popupID)).
434+ Str("parent_uri_at_open", logging.TruncateURL(parentURIAtOpen, logURLMaxLen)).
435+ Str("current_parent_uri", logging.TruncateURL(parentURI, logURLMaxLen)).
455436 Str("callback_uri", logging.TruncateURL(callbackURI, logURLMaxLen)).
456- Msg("loaded oauth callback URI in parent pane")
437+ Msg("skipping parent pane oauth resume: parent already navigated")
457438 return
458439 }
459440
@@ -469,5 +450,6 @@ func (c *Coordinator) resumeParentPaneAfterOAuth(
469450 log.Info().
470451 Str("parent_pane_id", string(parentPaneID)).
471452 Uint64("popup_id", uint64(popupID)).
453+ Str("callback_uri", logging.TruncateURL(callbackURI, logURLMaxLen)).
472454 Msg("refreshed parent pane after oauth popup close")
473455 }
@@ -9,7 +9,6 @@ import (
9 9
10 "github.com/bnema/dumber/internal/application/port"10 "github.com/bnema/dumber/internal/application/port"
11 "github.com/bnema/dumber/internal/domain/entity"11 "github.com/bnema/dumber/internal/domain/entity"
12- urlutil "github.com/bnema/dumber/internal/domain/url"
13 "github.com/bnema/dumber/internal/logging"12 "github.com/bnema/dumber/internal/logging"
14 "github.com/bnema/puregotk/v4/glib"13 "github.com/bnema/puregotk/v4/glib"
15 )14 )
@@ -324,14 +323,15 @@ func (c *Coordinator) setupOAuthAutoClose(
324 })323 })
325 }324 }
326 325
327-func (c *Coordinator) trackOAuthPopup(popupID port.WebViewID, parentPaneID entity.PaneID) {326+func (c *Coordinator) trackOAuthPopup(popupID port.WebViewID, parentPaneID entity.PaneID, parentURIAtOpen string) {
328 c.popupMu.Lock()327 c.popupMu.Lock()
329 defer c.popupMu.Unlock()328 defer c.popupMu.Unlock()
330 if c.popupOAuth == nil {329 if c.popupOAuth == nil {
331 c.popupOAuth = make(map[port.WebViewID]*popupOAuthState)330 c.popupOAuth = make(map[port.WebViewID]*popupOAuthState)
332 }331 }
333 c.popupOAuth[popupID] = &popupOAuthState{332 c.popupOAuth[popupID] = &popupOAuthState{
334- ParentPaneID: parentPaneID,333+ ParentPaneID: parentPaneID,
334+ ParentURIAtOpen: strings.TrimSpace(parentURIAtOpen),
335 }335 }
336 }336 }
337 337
@@ -350,15 +350,6 @@ func (c *Coordinator) capturePopupOAuthState(popupID port.WebViewID, uri string)
350 state.Error = IsOAuthError(uri)350 state.Error = IsOAuthError(uri)
351 }351 }
352 352
353-func shouldLoadOAuthCallbackInParent(parentURI, callbackURI string) bool {
354- if callbackURI == "" || !IsOAuthCallback(callbackURI) {
355- return false
356- }
357- parentDomain := urlutil.ExtractDomain(parentURI)
358- callbackDomain := urlutil.ExtractDomain(callbackURI)
359- return parentDomain != "" && parentDomain == callbackDomain
360-}
361-
362 func (c *Coordinator) handlePopupOAuthClose(ctx context.Context, popupID port.WebViewID) {353 func (c *Coordinator) handlePopupOAuthClose(ctx context.Context, popupID port.WebViewID) {
363 log := logging.FromContext(ctx)354 log := logging.FromContext(ctx)
364 355
@@ -384,21 +375,18 @@ func (c *Coordinator) handlePopupOAuthClose(ctx context.Context, popupID port.We
384 return375 return
385 }376 }
386 377
387- if strings.TrimSpace(state.CallbackURI) != "" {
388- c.scheduleParentPaneOAuthResume(ctx, state.ParentPaneID, popupID, state.CallbackURI)
389- return
390- }
391 if !state.Success {378 if !state.Success {
392 return379 return
393 }380 }
394 381
395- c.scheduleParentPaneOAuthResume(ctx, state.ParentPaneID, popupID, "")382+ c.scheduleParentPaneOAuthResume(ctx, state.ParentPaneID, popupID, state.ParentURIAtOpen, state.CallbackURI)
396 }383 }
397 384
398 func (c *Coordinator) scheduleParentPaneOAuthResume(385 func (c *Coordinator) scheduleParentPaneOAuthResume(
399 ctx context.Context,386 ctx context.Context,
400 parentPaneID entity.PaneID,387 parentPaneID entity.PaneID,
401 popupID port.WebViewID,388 popupID port.WebViewID,
389+ parentURIAtOpen string,
402 callbackURI string,390 callbackURI string,
403 ) {391 ) {
404 c.popupMu.Lock()392 c.popupMu.Lock()
@@ -413,7 +401,7 @@ func (c *Coordinator) scheduleParentPaneOAuthResume(
413 delete(c.popupRefresh, parentPaneID)401 delete(c.popupRefresh, parentPaneID)
414 c.popupMu.Unlock()402 c.popupMu.Unlock()
415 cb := glib.SourceFunc(func(_ uintptr) bool {403 cb := glib.SourceFunc(func(_ uintptr) bool {
416- c.resumeParentPaneAfterOAuth(ctx, parentPaneID, popupID, callbackURI)404+ c.resumeParentPaneAfterOAuth(ctx, parentPaneID, popupID, parentURIAtOpen, callbackURI)
417 return false405 return false
418 })406 })
419 glib.IdleAdd(&cb, 0)407 glib.IdleAdd(&cb, 0)
@@ -425,6 +413,7 @@ func (c *Coordinator) resumeParentPaneAfterOAuth(
425 ctx context.Context,413 ctx context.Context,
426 parentPaneID entity.PaneID,414 parentPaneID entity.PaneID,
427 popupID port.WebViewID,415 popupID port.WebViewID,
416+ parentURIAtOpen string,
428 callbackURI string,417 callbackURI string,
429 ) {418 ) {
430 log := logging.FromContext(ctx)419 log := logging.FromContext(ctx)
@@ -437,23 +426,15 @@ func (c *Coordinator) resumeParentPaneAfterOAuth(
437 return426 return
438 }427 }
439 428
440- parentURI := wv.URI()429+ parentURI := strings.TrimSpace(wv.URI())
441- if shouldLoadOAuthCallbackInParent(parentURI, callbackURI) {430+ if parentURIAtOpen != "" && parentURI != "" && parentURI != parentURIAtOpen {
442- if err := wv.LoadURI(ctx, callbackURI); err != nil {
443- log.Warn().
444- Err(err).
445- Str("parent_pane_id", string(parentPaneID)).
446- Uint64("popup_id", uint64(popupID)).
447- Str("callback_uri", logging.TruncateURL(callbackURI, logURLMaxLen)).
448- Msg("failed to load oauth callback URI in parent pane")
449- return
450- }
451-
452 log.Info().431 log.Info().
453 Str("parent_pane_id", string(parentPaneID)).432 Str("parent_pane_id", string(parentPaneID)).
454 Uint64("popup_id", uint64(popupID)).433 Uint64("popup_id", uint64(popupID)).
434+ Str("parent_uri_at_open", logging.TruncateURL(parentURIAtOpen, logURLMaxLen)).
435+ Str("current_parent_uri", logging.TruncateURL(parentURI, logURLMaxLen)).
455 Str("callback_uri", logging.TruncateURL(callbackURI, logURLMaxLen)).436 Str("callback_uri", logging.TruncateURL(callbackURI, logURLMaxLen)).
456- Msg("loaded oauth callback URI in parent pane")437+ Msg("skipping parent pane oauth resume: parent already navigated")
457 return438 return
458 }439 }
459 440
@@ -469,5 +450,6 @@ func (c *Coordinator) resumeParentPaneAfterOAuth(
469 log.Info().450 log.Info().
470 Str("parent_pane_id", string(parentPaneID)).451 Str("parent_pane_id", string(parentPaneID)).
471 Uint64("popup_id", uint64(popupID)).452 Uint64("popup_id", uint64(popupID)).
453+ Str("callback_uri", logging.TruncateURL(callbackURI, logURLMaxLen)).
472 Msg("refreshed parent pane after oauth popup close")454 Msg("refreshed parent pane after oauth popup close")
473 }455 }
M internal/ui/coordinator/content/oauth_test.go +22 -6
@@ -59,7 +59,7 @@ func TestHandlePopupOAuthClose_SuccessSchedulesParentResume(t *testing.T) {
5959 popupRefresh: make(map[entity.PaneID]*time.Timer),
6060 }
6161
62- c.trackOAuthPopup(popupID, parentPaneID)
62+ c.trackOAuthPopup(popupID, parentPaneID, "https://www.notion.so/login")
6363 c.capturePopupOAuthState(popupID, "https://www.notion.so/googlepopupcallback?code=123")
6464 c.handlePopupOAuthClose(context.Background(), popupID)
6565
@@ -78,13 +78,13 @@ func TestHandlePopupOAuthClose_SuccessSchedulesParentResume(t *testing.T) {
7878 })
7979 }
8080
81-func TestResumeParentPaneAfterOAuth_SameDomainLoadsCallbackURI(t *testing.T) {
81+func TestResumeParentPaneAfterOAuth_UnchangedParentFallsBackToReload(t *testing.T) {
8282 parentPaneID := entity.PaneID("parent-pane")
8383 popupID := port.WebViewID(102)
8484 parentWV := portmocks.NewMockWebView(t)
8585 parentWV.EXPECT().IsDestroyed().Return(false).Once()
8686 parentWV.EXPECT().URI().Return("https://www.notion.so/login").Once()
87- parentWV.EXPECT().LoadURI(mock.Anything, "https://www.notion.so/googlepopupcallback?code=123").Return(nil).Once()
87+ parentWV.EXPECT().Reload(mock.Anything).Return(nil).Once()
8888
8989 c := &Coordinator{
9090 webViews: map[entity.PaneID]port.WebView{
@@ -92,14 +92,30 @@ func TestResumeParentPaneAfterOAuth_SameDomainLoadsCallbackURI(t *testing.T) {
9292 },
9393 }
9494
95- c.resumeParentPaneAfterOAuth(context.Background(), parentPaneID, popupID, "https://www.notion.so/googlepopupcallback?code=123")
95+ c.resumeParentPaneAfterOAuth(context.Background(), parentPaneID, popupID, "https://www.notion.so/login", "https://www.notion.so/googlepopupcallback?code=123")
9696 }
9797
98-func TestResumeParentPaneAfterOAuth_DifferentDomainFallsBackToReload(t *testing.T) {
98+func TestResumeParentPaneAfterOAuth_ChangedParentSkipsIntervention(t *testing.T) {
9999 parentPaneID := entity.PaneID("parent-pane")
100100 popupID := port.WebViewID(103)
101101 parentWV := portmocks.NewMockWebView(t)
102102 parentWV.EXPECT().IsDestroyed().Return(false).Once()
103+ parentWV.EXPECT().URI().Return("https://www.notion.so/oauth2callback?state=abc").Once()
104+
105+ c := &Coordinator{
106+ webViews: map[entity.PaneID]port.WebView{
107+ parentPaneID: parentWV,
108+ },
109+ }
110+
111+ c.resumeParentPaneAfterOAuth(context.Background(), parentPaneID, popupID, "https://www.notion.so/login", "https://www.notion.so/googlepopupcallback?code=123")
112+}
113+
114+func TestResumeParentPaneAfterOAuth_DifferentDomainFallsBackToReload(t *testing.T) {
115+ parentPaneID := entity.PaneID("parent-pane")
116+ popupID := port.WebViewID(104)
117+ parentWV := portmocks.NewMockWebView(t)
118+ parentWV.EXPECT().IsDestroyed().Return(false).Once()
103119 parentWV.EXPECT().URI().Return("https://www.notion.so/login").Once()
104120 parentWV.EXPECT().Reload(mock.Anything).Return(nil).Once()
105121
@@ -109,7 +125,7 @@ func TestResumeParentPaneAfterOAuth_DifferentDomainFallsBackToReload(t *testing.
109125 },
110126 }
111127
112- c.resumeParentPaneAfterOAuth(context.Background(), parentPaneID, popupID, "https://accounts.example.com/callback?code=123")
128+ c.resumeParentPaneAfterOAuth(context.Background(), parentPaneID, popupID, "https://www.notion.so/login", "https://accounts.example.com/callback?code=123")
113129 }
114130
115131 func TestIsOAuthURL_DoesNotTreatGenericStateParamAsOAuth(t *testing.T) {
@@ -59,7 +59,7 @@ func TestHandlePopupOAuthClose_SuccessSchedulesParentResume(t *testing.T) {
59 popupRefresh: make(map[entity.PaneID]*time.Timer),59 popupRefresh: make(map[entity.PaneID]*time.Timer),
60 }60 }
61 61
62- c.trackOAuthPopup(popupID, parentPaneID)62+ c.trackOAuthPopup(popupID, parentPaneID, "https://www.notion.so/login")
63 c.capturePopupOAuthState(popupID, "https://www.notion.so/googlepopupcallback?code=123")63 c.capturePopupOAuthState(popupID, "https://www.notion.so/googlepopupcallback?code=123")
64 c.handlePopupOAuthClose(context.Background(), popupID)64 c.handlePopupOAuthClose(context.Background(), popupID)
65 65
@@ -78,13 +78,13 @@ func TestHandlePopupOAuthClose_SuccessSchedulesParentResume(t *testing.T) {
78 })78 })
79 }79 }
80 80
81-func TestResumeParentPaneAfterOAuth_SameDomainLoadsCallbackURI(t *testing.T) {81+func TestResumeParentPaneAfterOAuth_UnchangedParentFallsBackToReload(t *testing.T) {
82 parentPaneID := entity.PaneID("parent-pane")82 parentPaneID := entity.PaneID("parent-pane")
83 popupID := port.WebViewID(102)83 popupID := port.WebViewID(102)
84 parentWV := portmocks.NewMockWebView(t)84 parentWV := portmocks.NewMockWebView(t)
85 parentWV.EXPECT().IsDestroyed().Return(false).Once()85 parentWV.EXPECT().IsDestroyed().Return(false).Once()
86 parentWV.EXPECT().URI().Return("https://www.notion.so/login").Once()86 parentWV.EXPECT().URI().Return("https://www.notion.so/login").Once()
87- parentWV.EXPECT().LoadURI(mock.Anything, "https://www.notion.so/googlepopupcallback?code=123").Return(nil).Once()87+ parentWV.EXPECT().Reload(mock.Anything).Return(nil).Once()
88 88
89 c := &Coordinator{89 c := &Coordinator{
90 webViews: map[entity.PaneID]port.WebView{90 webViews: map[entity.PaneID]port.WebView{
@@ -92,14 +92,30 @@ func TestResumeParentPaneAfterOAuth_SameDomainLoadsCallbackURI(t *testing.T) {
92 },92 },
93 }93 }
94 94
95- c.resumeParentPaneAfterOAuth(context.Background(), parentPaneID, popupID, "https://www.notion.so/googlepopupcallback?code=123")95+ c.resumeParentPaneAfterOAuth(context.Background(), parentPaneID, popupID, "https://www.notion.so/login", "https://www.notion.so/googlepopupcallback?code=123")
96 }96 }
97 97
98-func TestResumeParentPaneAfterOAuth_DifferentDomainFallsBackToReload(t *testing.T) {98+func TestResumeParentPaneAfterOAuth_ChangedParentSkipsIntervention(t *testing.T) {
99 parentPaneID := entity.PaneID("parent-pane")99 parentPaneID := entity.PaneID("parent-pane")
100 popupID := port.WebViewID(103)100 popupID := port.WebViewID(103)
101 parentWV := portmocks.NewMockWebView(t)101 parentWV := portmocks.NewMockWebView(t)
102 parentWV.EXPECT().IsDestroyed().Return(false).Once()102 parentWV.EXPECT().IsDestroyed().Return(false).Once()
103+ parentWV.EXPECT().URI().Return("https://www.notion.so/oauth2callback?state=abc").Once()
104+
105+ c := &Coordinator{
106+ webViews: map[entity.PaneID]port.WebView{
107+ parentPaneID: parentWV,
108+ },
109+ }
110+
111+ c.resumeParentPaneAfterOAuth(context.Background(), parentPaneID, popupID, "https://www.notion.so/login", "https://www.notion.so/googlepopupcallback?code=123")
112+}
113+
114+func TestResumeParentPaneAfterOAuth_DifferentDomainFallsBackToReload(t *testing.T) {
115+ parentPaneID := entity.PaneID("parent-pane")
116+ popupID := port.WebViewID(104)
117+ parentWV := portmocks.NewMockWebView(t)
118+ parentWV.EXPECT().IsDestroyed().Return(false).Once()
103 parentWV.EXPECT().URI().Return("https://www.notion.so/login").Once()119 parentWV.EXPECT().URI().Return("https://www.notion.so/login").Once()
104 parentWV.EXPECT().Reload(mock.Anything).Return(nil).Once()120 parentWV.EXPECT().Reload(mock.Anything).Return(nil).Once()
105 121
@@ -109,7 +125,7 @@ func TestResumeParentPaneAfterOAuth_DifferentDomainFallsBackToReload(t *testing.
109 },125 },
110 }126 }
111 127
112- c.resumeParentPaneAfterOAuth(context.Background(), parentPaneID, popupID, "https://accounts.example.com/callback?code=123")128+ c.resumeParentPaneAfterOAuth(context.Background(), parentPaneID, popupID, "https://www.notion.so/login", "https://accounts.example.com/callback?code=123")
113 }129 }
114 130
115 func TestIsOAuthURL_DoesNotTreatGenericStateParamAsOAuth(t *testing.T) {131 func TestIsOAuthURL_DoesNotTreatGenericStateParamAsOAuth(t *testing.T) {
M internal/ui/coordinator/content/popup.go +7 -2
@@ -434,6 +434,10 @@ func (c *Coordinator) handlePopupCreate(
434434 }
435435
436436 parentID := parentWV.ID()
437+ parentURIAtOpen := ""
438+ if parent := c.getWebViewLocked(parentPaneID); parent != nil {
439+ parentURIAtOpen = parent.URI()
440+ }
437441 if !req.NoJavaScriptAccess {
438442 if reused, ok := c.reuseNamedPopup(ctx, parentPaneID, req.FrameName, req.TargetURI); ok {
439443 return reused
@@ -467,7 +471,7 @@ func (c *Coordinator) handlePopupCreate(
467471 // Create popup pane entity
468472 paneID, popupPane := c.createPopupPane(popupID, parentPaneID, req.TargetURI)
469473
470- return c.finishPopupCreate(ctx, parentPaneID, parentID, popupID, popupWV, popupPane, paneID, popupType, behavior, placement, req)
474+ return c.finishPopupCreate(ctx, parentPaneID, parentID, parentURIAtOpen, popupID, popupWV, popupPane, paneID, popupType, behavior, placement, req)
471475 }
472476
473477 func (c *Coordinator) reuseNamedPopup(
@@ -500,6 +504,7 @@ func (c *Coordinator) finishPopupCreate(
500504 ctx context.Context,
501505 parentPaneID entity.PaneID,
502506 parentID port.WebViewID,
507+ parentURIAtOpen string,
503508 popupID port.WebViewID,
504509 popupWV port.WebView,
505510 popupPane *entity.Pane,
@@ -564,7 +569,7 @@ func (c *Coordinator) finishPopupCreate(
564569 // Setup OAuth auto-close if configured
565570 if hasConfig && oauthEnabled && isOAuth {
566571 popupPane.AutoClose = true
567- c.trackOAuthPopup(popupID, parentPaneID)
572+ c.trackOAuthPopup(popupID, parentPaneID, parentURIAtOpen)
568573 c.setupOAuthAutoClose(ctx, paneID, popupID, popupWV)
569574 log.Debug().Str("pane_id", string(paneID)).Msg("OAuth auto-close enabled for popup")
570575 }
@@ -434,6 +434,10 @@ func (c *Coordinator) handlePopupCreate(
434 }434 }
435 435
436 parentID := parentWV.ID()436 parentID := parentWV.ID()
437+ parentURIAtOpen := ""
438+ if parent := c.getWebViewLocked(parentPaneID); parent != nil {
439+ parentURIAtOpen = parent.URI()
440+ }
437 if !req.NoJavaScriptAccess {441 if !req.NoJavaScriptAccess {
438 if reused, ok := c.reuseNamedPopup(ctx, parentPaneID, req.FrameName, req.TargetURI); ok {442 if reused, ok := c.reuseNamedPopup(ctx, parentPaneID, req.FrameName, req.TargetURI); ok {
439 return reused443 return reused
@@ -467,7 +471,7 @@ func (c *Coordinator) handlePopupCreate(
467 // Create popup pane entity471 // Create popup pane entity
468 paneID, popupPane := c.createPopupPane(popupID, parentPaneID, req.TargetURI)472 paneID, popupPane := c.createPopupPane(popupID, parentPaneID, req.TargetURI)
469 473
470- return c.finishPopupCreate(ctx, parentPaneID, parentID, popupID, popupWV, popupPane, paneID, popupType, behavior, placement, req)474+ return c.finishPopupCreate(ctx, parentPaneID, parentID, parentURIAtOpen, popupID, popupWV, popupPane, paneID, popupType, behavior, placement, req)
471 }475 }
472 476
473 func (c *Coordinator) reuseNamedPopup(477 func (c *Coordinator) reuseNamedPopup(
@@ -500,6 +504,7 @@ func (c *Coordinator) finishPopupCreate(
500 ctx context.Context,504 ctx context.Context,
501 parentPaneID entity.PaneID,505 parentPaneID entity.PaneID,
502 parentID port.WebViewID,506 parentID port.WebViewID,
507+ parentURIAtOpen string,
503 popupID port.WebViewID,508 popupID port.WebViewID,
504 popupWV port.WebView,509 popupWV port.WebView,
505 popupPane *entity.Pane,510 popupPane *entity.Pane,
@@ -564,7 +569,7 @@ func (c *Coordinator) finishPopupCreate(
564 // Setup OAuth auto-close if configured569 // Setup OAuth auto-close if configured
565 if hasConfig && oauthEnabled && isOAuth {570 if hasConfig && oauthEnabled && isOAuth {
566 popupPane.AutoClose = true571 popupPane.AutoClose = true
567- c.trackOAuthPopup(popupID, parentPaneID)572+ c.trackOAuthPopup(popupID, parentPaneID, parentURIAtOpen)
568 c.setupOAuthAutoClose(ctx, paneID, popupID, popupWV)573 c.setupOAuthAutoClose(ctx, paneID, popupID, popupWV)
569 log.Debug().Str("pane_id", string(paneID)).Msg("OAuth auto-close enabled for popup")574 log.Debug().Str("pane_id", string(paneID)).Msg("OAuth auto-close enabled for popup")
570 }575 }
M internal/ui/coordinator/content/popup_test.go +4 -3
@@ -388,7 +388,7 @@ func TestTrackOAuthPopup_StoresState(t *testing.T) {
388388 popupID := port.WebViewID(1)
389389 parentPaneID := entity.PaneID("parent-pane")
390390
391- c.trackOAuthPopup(popupID, parentPaneID)
391+ c.trackOAuthPopup(popupID, parentPaneID, "https://parent.example.com/login")
392392
393393 c.popupMu.RLock()
394394 state, ok := c.popupOAuth[popupID]
@@ -397,6 +397,7 @@ func TestTrackOAuthPopup_StoresState(t *testing.T) {
397397 require.True(t, ok, "oauth state should be tracked after trackOAuthPopup")
398398 require.NotNil(t, state)
399399 assert.Equal(t, parentPaneID, state.ParentPaneID)
400+ assert.Equal(t, "https://parent.example.com/login", state.ParentURIAtOpen)
400401 assert.False(t, state.Seen, "Seen should be false before capturing callback URI")
401402 }
402403
@@ -409,7 +410,7 @@ func TestCapturePopupOAuthState_SuccessCallback(t *testing.T) {
409410 }
410411
411412 popupID := port.WebViewID(2)
412- c.trackOAuthPopup(popupID, entity.PaneID("parent"))
413+ c.trackOAuthPopup(popupID, entity.PaneID("parent"), "https://parent.example.com/login")
413414 c.capturePopupOAuthState(popupID, "https://app.example.com/callback?code=abc123")
414415
415416 c.popupMu.RLock()
@@ -432,7 +433,7 @@ func TestCapturePopupOAuthState_ErrorCallback(t *testing.T) {
432433 }
433434
434435 popupID := port.WebViewID(3)
435- c.trackOAuthPopup(popupID, entity.PaneID("parent"))
436+ c.trackOAuthPopup(popupID, entity.PaneID("parent"), "https://parent.example.com/login")
436437 c.capturePopupOAuthState(popupID, "https://app.example.com/callback?error=access_denied")
437438
438439 c.popupMu.RLock()
@@ -388,7 +388,7 @@ func TestTrackOAuthPopup_StoresState(t *testing.T) {
388 popupID := port.WebViewID(1)388 popupID := port.WebViewID(1)
389 parentPaneID := entity.PaneID("parent-pane")389 parentPaneID := entity.PaneID("parent-pane")
390 390
391- c.trackOAuthPopup(popupID, parentPaneID)391+ c.trackOAuthPopup(popupID, parentPaneID, "https://parent.example.com/login")
392 392
393 c.popupMu.RLock()393 c.popupMu.RLock()
394 state, ok := c.popupOAuth[popupID]394 state, ok := c.popupOAuth[popupID]
@@ -397,6 +397,7 @@ func TestTrackOAuthPopup_StoresState(t *testing.T) {
397 require.True(t, ok, "oauth state should be tracked after trackOAuthPopup")397 require.True(t, ok, "oauth state should be tracked after trackOAuthPopup")
398 require.NotNil(t, state)398 require.NotNil(t, state)
399 assert.Equal(t, parentPaneID, state.ParentPaneID)399 assert.Equal(t, parentPaneID, state.ParentPaneID)
400+ assert.Equal(t, "https://parent.example.com/login", state.ParentURIAtOpen)
400 assert.False(t, state.Seen, "Seen should be false before capturing callback URI")401 assert.False(t, state.Seen, "Seen should be false before capturing callback URI")
401 }402 }
402 403
@@ -409,7 +410,7 @@ func TestCapturePopupOAuthState_SuccessCallback(t *testing.T) {
409 }410 }
410 411
411 popupID := port.WebViewID(2)412 popupID := port.WebViewID(2)
412- c.trackOAuthPopup(popupID, entity.PaneID("parent"))413+ c.trackOAuthPopup(popupID, entity.PaneID("parent"), "https://parent.example.com/login")
413 c.capturePopupOAuthState(popupID, "https://app.example.com/callback?code=abc123")414 c.capturePopupOAuthState(popupID, "https://app.example.com/callback?code=abc123")
414 415
415 c.popupMu.RLock()416 c.popupMu.RLock()
@@ -432,7 +433,7 @@ func TestCapturePopupOAuthState_ErrorCallback(t *testing.T) {
432 }433 }
433 434
434 popupID := port.WebViewID(3)435 popupID := port.WebViewID(3)
435- c.trackOAuthPopup(popupID, entity.PaneID("parent"))436+ c.trackOAuthPopup(popupID, entity.PaneID("parent"), "https://parent.example.com/login")
436 c.capturePopupOAuthState(popupID, "https://app.example.com/callback?error=access_denied")437 c.capturePopupOAuthState(popupID, "https://app.example.com/callback?error=access_denied")
437 438
438 c.popupMu.RLock()439 c.popupMu.RLock()

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