detached dbb6e05

fix: disable oauth safety-timeout forced popup close

Force-closing popup webviews on timeout can interrupt active MFA flows
and trigger unstable teardown paths.

Disable timeout-driven forced close and keep OAuth popup closure driven by
callback detection or manual user action.
commit
dbb6e05a5ad417bdfa09f9c5726337f8238f4983
author
brice · · 2026-02-17 05:15 UTC
parent
64a59d7
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +32 -2
M internal/ui/coordinator/content.go +10 -2
@@ -1810,8 +1810,16 @@ func (c *ContentCoordinator) setupOAuthAutoClose(
18101810 safetyTimer = time.AfterFunc(oauthSafetyTimeout, func() {
18111811 cb := glib.SourceFunc(func(_ uintptr) bool {
18121812 if wv != nil && !wv.IsDestroyed() {
1813- log.Warn().Str("pane", string(paneID)).Msg("oauth safety timeout, closing stuck popup")
1814- wv.Close()
1813+ uri := wv.URI()
1814+ if shouldForceCloseOnSafetyTimeout(uri) {
1815+ log.Warn().Str("pane", string(paneID)).Msg("oauth safety timeout, closing stuck popup")
1816+ wv.Close()
1817+ return false
1818+ }
1819+ log.Debug().
1820+ Str("pane", string(paneID)).
1821+ Str("uri", logging.TruncateURL(uri, logURLMaxLen)).
1822+ Msg("oauth safety timeout reached during active auth flow, skipping forced close")
18151823 }
18161824 return false
18171825 })
@@ -1810,8 +1810,16 @@ func (c *ContentCoordinator) setupOAuthAutoClose(
1810 safetyTimer = time.AfterFunc(oauthSafetyTimeout, func() {1810 safetyTimer = time.AfterFunc(oauthSafetyTimeout, func() {
1811 cb := glib.SourceFunc(func(_ uintptr) bool {1811 cb := glib.SourceFunc(func(_ uintptr) bool {
1812 if wv != nil && !wv.IsDestroyed() {1812 if wv != nil && !wv.IsDestroyed() {
1813- log.Warn().Str("pane", string(paneID)).Msg("oauth safety timeout, closing stuck popup")1813+ uri := wv.URI()
1814- wv.Close()1814+ if shouldForceCloseOnSafetyTimeout(uri) {
1815+ log.Warn().Str("pane", string(paneID)).Msg("oauth safety timeout, closing stuck popup")
1816+ wv.Close()
1817+ return false
1818+ }
1819+ log.Debug().
1820+ Str("pane", string(paneID)).
1821+ Str("uri", logging.TruncateURL(uri, logURLMaxLen)).
1822+ Msg("oauth safety timeout reached during active auth flow, skipping forced close")
1815 }1823 }
1816 return false1824 return false
1817 })1825 })
M internal/ui/coordinator/oauth.go +10 -0
@@ -105,6 +105,16 @@ func ShouldAutoClose(url string) bool {
105105 return IsOAuthCallback(url)
106106 }
107107
108+// shouldForceCloseOnSafetyTimeout determines if a popup should be force-closed
109+// when the OAuth safety timeout is reached.
110+//
111+// Safety-timeout force close is disabled for stability. OAuth popups close on
112+// callback detection or manual user action.
113+func shouldForceCloseOnSafetyTimeout(url string) bool {
114+ _ = url
115+ return false
116+}
117+
108118 // IsOAuthSuccess checks if the callback indicates successful authentication.
109119 func IsOAuthSuccess(url string) bool {
110120 if url == "" {
@@ -105,6 +105,16 @@ func ShouldAutoClose(url string) bool {
105 return IsOAuthCallback(url)105 return IsOAuthCallback(url)
106 }106 }
107 107
108+// shouldForceCloseOnSafetyTimeout determines if a popup should be force-closed
109+// when the OAuth safety timeout is reached.
110+//
111+// Safety-timeout force close is disabled for stability. OAuth popups close on
112+// callback detection or manual user action.
113+func shouldForceCloseOnSafetyTimeout(url string) bool {
114+ _ = url
115+ return false
116+}
117+
108 // IsOAuthSuccess checks if the callback indicates successful authentication.118 // IsOAuthSuccess checks if the callback indicates successful authentication.
109 func IsOAuthSuccess(url string) bool {119 func IsOAuthSuccess(url string) bool {
110 if url == "" {120 if url == "" {
M internal/ui/coordinator/oauth_test.go +12 -0
@@ -108,6 +108,18 @@ func TestIsOAuthURL_DetectsOAuthAuthorizeRequest(t *testing.T) {
108108 assert.True(t, IsOAuthURL(url))
109109 }
110110
111+func TestShouldForceCloseOnSafetyTimeout(t *testing.T) {
112+ t.Run("does not force close during in-progress auth challenge", func(t *testing.T) {
113+ url := "https://accounts.google.com/v3/signin/challenge/dp?TL=AHU8sQabc"
114+ assert.False(t, shouldForceCloseOnSafetyTimeout(url))
115+ })
116+
117+ t.Run("does not force close when callback is reached", func(t *testing.T) {
118+ url := "http://localhost:1455/auth/callback?code=abc123&state=xyz"
119+ assert.False(t, shouldForceCloseOnSafetyTimeout(url))
120+ })
121+}
122+
111123 func waitFor(t *testing.T, timeout time.Duration, condition func() bool) {
112124 t.Helper()
113125
@@ -108,6 +108,18 @@ func TestIsOAuthURL_DetectsOAuthAuthorizeRequest(t *testing.T) {
108 assert.True(t, IsOAuthURL(url))108 assert.True(t, IsOAuthURL(url))
109 }109 }
110 110
111+func TestShouldForceCloseOnSafetyTimeout(t *testing.T) {
112+ t.Run("does not force close during in-progress auth challenge", func(t *testing.T) {
113+ url := "https://accounts.google.com/v3/signin/challenge/dp?TL=AHU8sQabc"
114+ assert.False(t, shouldForceCloseOnSafetyTimeout(url))
115+ })
116+
117+ t.Run("does not force close when callback is reached", func(t *testing.T) {
118+ url := "http://localhost:1455/auth/callback?code=abc123&state=xyz"
119+ assert.False(t, shouldForceCloseOnSafetyTimeout(url))
120+ })
121+}
122+
111 func waitFor(t *testing.T, timeout time.Duration, condition func() bool) {123 func waitFor(t *testing.T, timeout time.Duration, condition func() bool) {
112 t.Helper()124 t.Helper()
113 125

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