diff --git a/internal/auth/gemini/gemini_auth.go b/internal/auth/gemini/gemini_auth.go index c459c5ca..2995a1cb 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,14 @@ 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 + manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the Gemini callback URL (or press Enter to keep waiting): ") + 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 +349,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/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 6ed31d6d..d52bf1d2 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,11 @@ 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 - } + manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the antigravity callback URL (or press Enter to keep waiting): ") + continue + case input := <-manualInputCh: + manualInputCh = nil + manualInputErrCh = nil parsed, errParse := misc.ParseOAuthCallback(input) if errParse != nil { return nil, errParse @@ -132,6 +136,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..d82a718b 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,11 @@ 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 - } + manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the Claude callback URL (or press Enter to keep waiting): ") + continue + case input := <-manualInputCh: + manualInputCh = nil + manualInputErrCh = nil parsed, errParse := misc.ParseOAuthCallback(input) if errParse != nil { return nil, errParse @@ -167,6 +171,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..269e3d8b 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,11 @@ 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 - } + manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the Codex callback URL (or press Enter to keep waiting): ") + continue + case input := <-manualInputCh: + manualInputCh = nil + manualInputErrCh = nil parsed, errParse := misc.ParseOAuthCallback(input) if errParse != nil { return nil, errParse @@ -170,6 +174,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..584a3169 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,11 @@ 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 - } + manualInputCh, manualInputErrCh = misc.AsyncPrompt(opts.Prompt, "Paste the iFlow callback URL (or press Enter to keep waiting): ") + continue + case input := <-manualInputCh: + manualInputCh = nil + manualInputErrCh = nil parsed, errParse := misc.ParseOAuthCallback(input) if errParse != nil { return nil, errParse @@ -145,6 +149,8 @@ waitForCallback: Error: parsed.Error, } break waitForCallback + case errManual := <-manualInputErrCh: + return nil, errManual } } if result.Error != "" {