From cccb77b552af59be55df2475115ac4ea2e6a963b Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:48:30 +0800 Subject: [PATCH 1/2] fix(auth): avoid blocking oauth callback wait on prompt --- internal/auth/gemini/gemini_auth.go | 31 ++++++++++++++++++++++------- sdk/auth/antigravity.go | 25 +++++++++++++++++++---- sdk/auth/claude.go | 25 +++++++++++++++++++---- sdk/auth/codex.go | 25 +++++++++++++++++++---- sdk/auth/iflow.go | 25 +++++++++++++++++++---- 5 files changed, 108 insertions(+), 23 deletions(-) diff --git a/internal/auth/gemini/gemini_auth.go b/internal/auth/gemini/gemini_auth.go index c459c5ca..0830e0a2 100644 --- a/internal/auth/gemini/gemini_auth.go +++ b/internal/auth/gemini/gemini_auth.go @@ -305,6 +305,9 @@ func (g *GeminiAuth) getTokenFromWeb(ctx context.Context, config *oauth2.Config, defer manualPromptTimer.Stop() } + var manualInputCh <-chan string + var manualInputErrCh <-chan error + waitForCallback: for { select { @@ -326,13 +329,25 @@ waitForCallback: return nil, err default: } - input, err := opts.Prompt("Paste the Gemini callback URL (or press Enter to keep waiting): ") - if err != nil { - return nil, err - } - parsed, err := misc.ParseOAuthCallback(input) - if err != nil { - return nil, err + inputCh := make(chan string, 1) + inputErrCh := make(chan error, 1) + go func() { + input, err := opts.Prompt("Paste the Gemini callback URL (or press Enter to keep waiting): ") + if err != nil { + inputErrCh <- err + return + } + inputCh <- input + }() + manualInputCh = inputCh + manualInputErrCh = inputErrCh + continue + case input := <-manualInputCh: + manualInputCh = nil + manualInputErrCh = nil + parsed, errParse := misc.ParseOAuthCallback(input) + if errParse != nil { + return nil, errParse } if parsed == nil { continue @@ -345,6 +360,8 @@ waitForCallback: } authCode = parsed.Code break waitForCallback + case errManual := <-manualInputErrCh: + return nil, errManual case <-timeoutTimer.C: return nil, fmt.Errorf("oauth flow timed out") } diff --git a/sdk/auth/antigravity.go b/sdk/auth/antigravity.go index 6ed31d6d..38617868 100644 --- a/sdk/auth/antigravity.go +++ b/sdk/auth/antigravity.go @@ -98,6 +98,9 @@ func (AntigravityAuthenticator) Login(ctx context.Context, cfg *config.Config, o defer manualPromptTimer.Stop() } + var manualInputCh <-chan string + var manualInputErrCh <-chan error + waitForCallback: for { select { @@ -115,10 +118,22 @@ waitForCallback: break waitForCallback default: } - input, errPrompt := opts.Prompt("Paste the antigravity callback URL (or press Enter to keep waiting): ") - if errPrompt != nil { - return nil, errPrompt - } + inputCh := make(chan string, 1) + inputErrCh := make(chan error, 1) + go func() { + input, errPrompt := opts.Prompt("Paste the antigravity callback URL (or press Enter to keep waiting): ") + if errPrompt != nil { + inputErrCh <- errPrompt + return + } + inputCh <- input + }() + manualInputCh = inputCh + manualInputErrCh = inputErrCh + continue + case input := <-manualInputCh: + manualInputCh = nil + manualInputErrCh = nil parsed, errParse := misc.ParseOAuthCallback(input) if errParse != nil { return nil, errParse @@ -132,6 +147,8 @@ waitForCallback: Error: parsed.Error, } break waitForCallback + case errManual := <-manualInputErrCh: + return nil, errManual case <-timeoutTimer.C: return nil, fmt.Errorf("antigravity: authentication timed out") } diff --git a/sdk/auth/claude.go b/sdk/auth/claude.go index 706763b3..4e54a99f 100644 --- a/sdk/auth/claude.go +++ b/sdk/auth/claude.go @@ -124,6 +124,9 @@ func (a *ClaudeAuthenticator) Login(ctx context.Context, cfg *config.Config, opt defer manualPromptTimer.Stop() } + var manualInputCh <-chan string + var manualInputErrCh <-chan error + waitForCallback: for { select { @@ -149,10 +152,22 @@ waitForCallback: return nil, err default: } - input, errPrompt := opts.Prompt("Paste the Claude callback URL (or press Enter to keep waiting): ") - if errPrompt != nil { - return nil, errPrompt - } + inputCh := make(chan string, 1) + inputErrCh := make(chan error, 1) + go func() { + input, errPrompt := opts.Prompt("Paste the Claude callback URL (or press Enter to keep waiting): ") + if errPrompt != nil { + inputErrCh <- errPrompt + return + } + inputCh <- input + }() + manualInputCh = inputCh + manualInputErrCh = inputErrCh + continue + case input := <-manualInputCh: + manualInputCh = nil + manualInputErrCh = nil parsed, errParse := misc.ParseOAuthCallback(input) if errParse != nil { return nil, errParse @@ -167,6 +182,8 @@ waitForCallback: Error: parsed.Error, } break waitForCallback + case errManual := <-manualInputErrCh: + return nil, errManual } } diff --git a/sdk/auth/codex.go b/sdk/auth/codex.go index 1af36936..7a6e0d38 100644 --- a/sdk/auth/codex.go +++ b/sdk/auth/codex.go @@ -127,6 +127,9 @@ func (a *CodexAuthenticator) Login(ctx context.Context, cfg *config.Config, opts defer manualPromptTimer.Stop() } + var manualInputCh <-chan string + var manualInputErrCh <-chan error + waitForCallback: for { select { @@ -152,10 +155,22 @@ waitForCallback: return nil, err default: } - input, errPrompt := opts.Prompt("Paste the Codex callback URL (or press Enter to keep waiting): ") - if errPrompt != nil { - return nil, errPrompt - } + inputCh := make(chan string, 1) + inputErrCh := make(chan error, 1) + go func() { + input, errPrompt := opts.Prompt("Paste the Codex callback URL (or press Enter to keep waiting): ") + if errPrompt != nil { + inputErrCh <- errPrompt + return + } + inputCh <- input + }() + manualInputCh = inputCh + manualInputErrCh = inputErrCh + continue + case input := <-manualInputCh: + manualInputCh = nil + manualInputErrCh = nil parsed, errParse := misc.ParseOAuthCallback(input) if errParse != nil { return nil, errParse @@ -170,6 +185,8 @@ waitForCallback: Error: parsed.Error, } break waitForCallback + case errManual := <-manualInputErrCh: + return nil, errManual } } diff --git a/sdk/auth/iflow.go b/sdk/auth/iflow.go index a695311d..0e7b5ce8 100644 --- a/sdk/auth/iflow.go +++ b/sdk/auth/iflow.go @@ -109,6 +109,9 @@ func (a *IFlowAuthenticator) Login(ctx context.Context, cfg *config.Config, opts defer manualPromptTimer.Stop() } + var manualInputCh <-chan string + var manualInputErrCh <-chan error + waitForCallback: for { select { @@ -128,10 +131,22 @@ waitForCallback: return nil, fmt.Errorf("iflow auth: callback wait failed: %w", err) default: } - input, errPrompt := opts.Prompt("Paste the iFlow callback URL (or press Enter to keep waiting): ") - if errPrompt != nil { - return nil, errPrompt - } + inputCh := make(chan string, 1) + inputErrCh := make(chan error, 1) + go func() { + input, errPrompt := opts.Prompt("Paste the iFlow callback URL (or press Enter to keep waiting): ") + if errPrompt != nil { + inputErrCh <- errPrompt + return + } + inputCh <- input + }() + manualInputCh = inputCh + manualInputErrCh = inputErrCh + continue + case input := <-manualInputCh: + manualInputCh = nil + manualInputErrCh = nil parsed, errParse := misc.ParseOAuthCallback(input) if errParse != nil { return nil, errParse @@ -145,6 +160,8 @@ waitForCallback: Error: parsed.Error, } break waitForCallback + case errManual := <-manualInputErrCh: + return nil, errManual } } if result.Error != "" { From 636da4c932e1e0fe6e23cba5bb3030e341a76c21 Mon Sep 17 00:00:00 2001 From: hkfires <10558748+hkfires@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:24:27 +0800 Subject: [PATCH 2/2] refactor(auth): replace manual input handling with AsyncPrompt for callback URLs --- internal/auth/gemini/gemini_auth.go | 13 +------------ internal/misc/oauth.go | 17 +++++++++++++++++ sdk/auth/antigravity.go | 13 +------------ sdk/auth/claude.go | 13 +------------ sdk/auth/codex.go | 13 +------------ sdk/auth/iflow.go | 13 +------------ 6 files changed, 22 insertions(+), 60 deletions(-) diff --git a/internal/auth/gemini/gemini_auth.go b/internal/auth/gemini/gemini_auth.go index 0830e0a2..2995a1cb 100644 --- a/internal/auth/gemini/gemini_auth.go +++ b/internal/auth/gemini/gemini_auth.go @@ -329,18 +329,7 @@ waitForCallback: return nil, err default: } - inputCh := make(chan string, 1) - inputErrCh := make(chan error, 1) - go func() { - input, err := opts.Prompt("Paste the Gemini callback URL (or press Enter to keep waiting): ") - if err != nil { - inputErrCh <- err - return - } - inputCh <- input - }() - manualInputCh = inputCh - manualInputErrCh = inputErrCh + manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the Gemini callback URL (or press Enter to keep waiting): ") continue case input := <-manualInputCh: manualInputCh = nil diff --git a/internal/misc/oauth.go b/internal/misc/oauth.go index c14f39d2..88be2eef 100644 --- a/internal/misc/oauth.go +++ b/internal/misc/oauth.go @@ -30,6 +30,23 @@ type OAuthCallback struct { ErrorDescription string } +// AsyncPrompt runs a prompt function in a goroutine and returns channels for +// the result. The returned channels are buffered (size 1) so the goroutine can +// complete even if the caller abandons the channels. +func AsyncPrompt(promptFn func(string) (string, error), message string) (<-chan string, <-chan error) { + inputCh := make(chan string, 1) + errCh := make(chan error, 1) + go func() { + input, err := promptFn(message) + if err != nil { + errCh <- err + return + } + inputCh <- input + }() + return inputCh, errCh +} + // ParseOAuthCallback extracts OAuth parameters from a callback URL. // It returns nil when the input is empty. func ParseOAuthCallback(input string) (*OAuthCallback, error) { diff --git a/sdk/auth/antigravity.go b/sdk/auth/antigravity.go index 38617868..d52bf1d2 100644 --- a/sdk/auth/antigravity.go +++ b/sdk/auth/antigravity.go @@ -118,18 +118,7 @@ waitForCallback: break waitForCallback default: } - inputCh := make(chan string, 1) - inputErrCh := make(chan error, 1) - go func() { - input, errPrompt := opts.Prompt("Paste the antigravity callback URL (or press Enter to keep waiting): ") - if errPrompt != nil { - inputErrCh <- errPrompt - return - } - inputCh <- input - }() - manualInputCh = inputCh - manualInputErrCh = inputErrCh + manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the antigravity callback URL (or press Enter to keep waiting): ") continue case input := <-manualInputCh: manualInputCh = nil diff --git a/sdk/auth/claude.go b/sdk/auth/claude.go index 4e54a99f..d82a718b 100644 --- a/sdk/auth/claude.go +++ b/sdk/auth/claude.go @@ -152,18 +152,7 @@ waitForCallback: return nil, err default: } - inputCh := make(chan string, 1) - inputErrCh := make(chan error, 1) - go func() { - input, errPrompt := opts.Prompt("Paste the Claude callback URL (or press Enter to keep waiting): ") - if errPrompt != nil { - inputErrCh <- errPrompt - return - } - inputCh <- input - }() - manualInputCh = inputCh - manualInputErrCh = inputErrCh + manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the Claude callback URL (or press Enter to keep waiting): ") continue case input := <-manualInputCh: manualInputCh = nil diff --git a/sdk/auth/codex.go b/sdk/auth/codex.go index 7a6e0d38..269e3d8b 100644 --- a/sdk/auth/codex.go +++ b/sdk/auth/codex.go @@ -155,18 +155,7 @@ waitForCallback: return nil, err default: } - inputCh := make(chan string, 1) - inputErrCh := make(chan error, 1) - go func() { - input, errPrompt := opts.Prompt("Paste the Codex callback URL (or press Enter to keep waiting): ") - if errPrompt != nil { - inputErrCh <- errPrompt - return - } - inputCh <- input - }() - manualInputCh = inputCh - manualInputErrCh = inputErrCh + manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the Codex callback URL (or press Enter to keep waiting): ") continue case input := <-manualInputCh: manualInputCh = nil diff --git a/sdk/auth/iflow.go b/sdk/auth/iflow.go index 0e7b5ce8..584a3169 100644 --- a/sdk/auth/iflow.go +++ b/sdk/auth/iflow.go @@ -131,18 +131,7 @@ waitForCallback: return nil, fmt.Errorf("iflow auth: callback wait failed: %w", err) default: } - inputCh := make(chan string, 1) - inputErrCh := make(chan error, 1) - go func() { - input, errPrompt := opts.Prompt("Paste the iFlow callback URL (or press Enter to keep waiting): ") - if errPrompt != nil { - inputErrCh <- errPrompt - return - } - inputCh <- input - }() - manualInputCh = inputCh - manualInputErrCh = inputErrCh + manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the iFlow callback URL (or press Enter to keep waiting): ") continue case input := <-manualInputCh: manualInputCh = nil