Skip to content

Commit a57c9e0

Browse files
Add Qwen OAuth support with email pre-collection (#14)
* Add Qwen OAuth support with email pre-collection - Add Qwen authentication with browser-based OAuth flow - Implement pre-auth email collection dialog for seamless UX - Add automatic email submission after OAuth completion - Add Qwen service section to settings UI with icon - Add auth status tracking for Qwen credentials - Increase settings window height (440px → 490px) - Update README and CHANGELOG for v1.0.6 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * Increase Qwen email submission delay to 10s with documentation - Increase OAuth completion delay from 5s to 10s for more reliable timing - Add detailed comment explaining why 10s was chosen - Document that OAuth typically completes in 5-8s but can vary - Note future improvement: monitor OAuth completion signal directly Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --------- Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
1 parent 6065058 commit a57c9e0

File tree

6 files changed

+164
-6
lines changed

6 files changed

+164
-6
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@ All notable changes to VibeProxy will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.0.6] - 2025-10-15
9+
10+
### Added
11+
- **Qwen Support** - Full integration with Qwen AI via OAuth authentication
12+
- Browser-based Qwen OAuth flow with automatic email submission
13+
- Pre-authentication email collection dialog for seamless UX
14+
- Automatic credential file creation with type: "qwen"
15+
- Connection status display with email and expiration tracking
16+
- Qwen added to end of service providers list
17+
18+
### Improved
19+
- **Settings Window** - Increased height from 440px to 490px to accommodate Qwen service section
20+
821
## [1.0.5] - 2025-10-14
922

1023
### Added
@@ -141,6 +154,7 @@ All future changes will be documented here before release.
141154

142155
---
143156

157+
[1.0.6]: https://github.com/automazeio/vibeproxy/releases/tag/v1.0.6
144158
[1.0.5]: https://github.com/automazeio/vibeproxy/releases/tag/v1.0.5
145159
[1.0.4]: https://github.com/automazeio/vibeproxy/releases/tag/v1.0.4
146160
[1.0.3]: https://github.com/automazeio/vibeproxy/releases/tag/v1.0.3

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@
1111
<a href="https://github.com/automazeio/vibeproxy"><img alt="Star this repo" src="https://img.shields.io/github/stars/automazeio/vibeproxy.svg?style=social&amp;label=Star%20this%20repo&amp;maxAge=60" style="max-width: 100%;"></a></p>
1212
</p>
1313

14-
**Stop paying twice for AI.** VibeProxy is a beautiful native macOS menu bar app that lets you use your existing Claude Code, ChatGPT, and **Gemini** subscriptions with powerful AI coding tools like **[Factory Droids](https://app.factory.ai/r/FM8BJHFQ)** – no separate API keys required.
14+
**Stop paying twice for AI.** VibeProxy is a beautiful native macOS menu bar app that lets you use your existing Claude Code, ChatGPT, **Gemini**, and **Qwen** subscriptions with powerful AI coding tools like **[Factory Droids](https://app.factory.ai/r/FM8BJHFQ)** – no separate API keys required.
1515

1616
Built on [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI), it handles OAuth authentication, token management, and API routing automatically. One click to authenticate, zero friction to code.
1717

1818
> [!IMPORTANT]
19-
> **NEW: Gemini Support! 🎉** VibeProxy now supports Google's Gemini AI with full OAuth authentication. Connect your Google account and use Gemini with your favorite AI coding tools!
19+
> **NEW: Gemini and Qwen Support! 🎉** VibeProxy now supports Google's Gemini AI and Qwen AI with full OAuth authentication. Connect your accounts and use Gemini and Qwen with your favorite AI coding tools!
2020
2121
> [!IMPORTANT]
2222
> **NEW: Extended Thinking Support! 🧠** VibeProxy now supports Claude's extended thinking feature with dynamic budgets (4K, 10K, 32K tokens). Use model names like `claude-sonnet-4-5-20250929-thinking-10000` to enable extended thinking. See the [Factory Setup Guide](FACTORY_SETUP.md#step-3-configure-factory-cli) for details.
@@ -34,7 +34,7 @@ Built on [CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI), it handles
3434

3535
- 🎯 **Native macOS Experience** - Clean, native SwiftUI interface that feels right at home on macOS
3636
- 🚀 **One-Click Server Management** - Start/stop the proxy server from your menu bar
37-
- 🔐 **OAuth Integration** - Authenticate with Codex, Claude Code, and Gemini directly from the app
37+
- 🔐 **OAuth Integration** - Authenticate with Codex, Claude Code, Gemini, and Qwen directly from the app
3838
- 📊 **Real-Time Status** - Live connection status and automatic credential detection
3939
- 🔄 **Auto-Updates** - Monitors auth files and updates UI in real-time
4040
- 🎨 **Beautiful Icons** - Custom icons with dark mode support
@@ -65,7 +65,7 @@ Want to build it yourself? See [**INSTALLATION.md**](INSTALLATION.md) for detail
6565
1. Launch VibeProxy - you'll see a menu bar icon
6666
2. Click the icon and select "Open Settings"
6767
3. The server will start automatically
68-
4. Click "Connect" for Claude Code, Codex, or Gemini to authenticate
68+
4. Click "Connect" for Claude Code, Codex, Gemini, or Qwen to authenticate
6969

7070
### Authentication
7171

@@ -106,7 +106,8 @@ VibeProxy/
106106
│ ├── icon-inactive.png # Menu bar icon (inactive)
107107
│ ├── icon-claude.png # Claude Code service icon
108108
│ ├── icon-codex.png # Codex service icon
109-
│ └── icon-gemini.png # Gemini service icon
109+
│ ├── icon-gemini.png # Gemini service icon
110+
│ └── icon-qwen.png # Qwen service icon
110111
├── Package.swift # Swift Package Manager config
111112
├── Info.plist # macOS app metadata
112113
├── build.sh # Resource bundling script

src/Sources/AuthStatus.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class AuthManager: ObservableObject {
2828
@Published var claudeStatus = AuthStatus(isAuthenticated: false, type: "claude")
2929
@Published var codexStatus = AuthStatus(isAuthenticated: false, type: "codex")
3030
@Published var geminiStatus = AuthStatus(isAuthenticated: false, type: "gemini")
31+
@Published var qwenStatus = AuthStatus(isAuthenticated: false, type: "qwen")
3132

3233
func checkAuthStatus() {
3334
let authDir = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".cli-proxy-api")
@@ -36,6 +37,7 @@ class AuthManager: ObservableObject {
3637
var foundClaude = false
3738
var foundCodex = false
3839
var foundGemini = false
40+
var foundQwen = false
3941

4042
// Check for auth files
4143
do {
@@ -79,6 +81,10 @@ class AuthManager: ObservableObject {
7981
foundGemini = true
8082
self.geminiStatus = status
8183
NSLog("[AuthStatus] Found Gemini auth: %@", email ?? "unknown")
84+
case "qwen":
85+
foundQwen = true
86+
self.qwenStatus = status
87+
NSLog("[AuthStatus] Found Qwen auth: %@", email ?? "unknown")
8288
default:
8389
break
8490
}
@@ -100,6 +106,10 @@ class AuthManager: ObservableObject {
100106
NSLog("[AuthStatus] No Gemini auth file found - resetting status")
101107
self.geminiStatus = AuthStatus(isAuthenticated: false, type: "gemini")
102108
}
109+
if !foundQwen {
110+
NSLog("[AuthStatus] No Qwen auth file found - resetting status")
111+
self.qwenStatus = AuthStatus(isAuthenticated: false, type: "qwen")
112+
}
103113
}
104114
} catch {
105115
NSLog("[AuthStatus] Error checking auth status: %@", error.localizedDescription)
@@ -108,6 +118,7 @@ class AuthManager: ObservableObject {
108118
self.claudeStatus = AuthStatus(isAuthenticated: false, type: "claude")
109119
self.codexStatus = AuthStatus(isAuthenticated: false, type: "codex")
110120
self.geminiStatus = AuthStatus(isAuthenticated: false, type: "gemini")
121+
self.qwenStatus = AuthStatus(isAuthenticated: false, type: "qwen")
111122
}
112123
}
113124
}
4.77 KB
Loading

src/Sources/ServerManager.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,13 +219,18 @@ class ServerManager {
219219
// Get the config path
220220
let configPath = (resourcePath as NSString).appendingPathComponent("config.yaml")
221221

222+
var qwenEmail: String?
223+
222224
switch command {
223225
case .claudeLogin:
224226
authProcess.arguments = ["--config", configPath, "-claude-login"]
225227
case .codexLogin:
226228
authProcess.arguments = ["--config", configPath, "-codex-login"]
227229
case .geminiLogin:
228230
authProcess.arguments = ["--config", configPath, "-login"]
231+
case .qwenLogin(let email):
232+
authProcess.arguments = ["--config", configPath, "-qwen-login"]
233+
qwenEmail = email
229234
}
230235

231236
// Create pipes for output
@@ -249,6 +254,23 @@ class ServerManager {
249254
}
250255
}
251256

257+
// For Qwen login, automatically send email after OAuth completes
258+
// NOTE: 10 second delay chosen to ensure OAuth browser flow completes before submitting email.
259+
// This is a conservative estimate - OAuth typically completes in 5-8 seconds, but network
260+
// conditions and user interaction time can vary. Future improvement: monitor authProcess
261+
// output or termination handler to detect OAuth completion signal and submit immediately.
262+
if let email = qwenEmail {
263+
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 10.0) {
264+
// Send email after OAuth completion
265+
if authProcess.isRunning {
266+
if let data = "\(email)\n".data(using: .utf8) {
267+
try? inputPipe.fileHandleForWriting.write(contentsOf: data)
268+
NSLog("[Auth] Sent Qwen email: %@", email)
269+
}
270+
}
271+
}
272+
}
273+
252274
// Set environment to inherit from parent
253275
authProcess.environment = ProcessInfo.processInfo.environment
254276

@@ -370,4 +392,5 @@ enum AuthCommand {
370392
case claudeLogin
371393
case codexLogin
372394
case geminiLogin
395+
case qwenLogin(email: String)
373396
}

src/Sources/SettingsView.swift

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ struct SettingsView: View {
88
@State private var isAuthenticatingClaude = false
99
@State private var isAuthenticatingCodex = false
1010
@State private var isAuthenticatingGemini = false
11+
@State private var isAuthenticatingQwen = false
1112
@State private var showingAuthResult = false
1213
@State private var authResultMessage = ""
1314
@State private var authResultSuccess = false
1415
@State private var fileMonitor: DispatchSourceFileSystemObject?
16+
@State private var showingQwenEmailPrompt = false
17+
@State private var qwenEmail = ""
1518

1619
private enum DisconnectTiming {
1720
static let serverRestartDelay: TimeInterval = 0.3
@@ -195,6 +198,49 @@ struct SettingsView: View {
195198
}
196199
}
197200
.help("⚠️ Note: If you're an existing Gemini user with multiple projects, authentication will use your default project. Set your desired project as default in Google AI Studio before connecting.")
201+
202+
HStack {
203+
if let nsImage = IconCatalog.shared.image(named: "icon-qwen.png", resizedTo: NSSize(width: 20, height: 20), template: true) {
204+
Image(nsImage: nsImage)
205+
.resizable()
206+
.renderingMode(.template)
207+
.frame(width: 20, height: 20)
208+
}
209+
VStack(alignment: .leading, spacing: 2) {
210+
Text("Qwen")
211+
if authManager.qwenStatus.isAuthenticated {
212+
Text(authManager.qwenStatus.email ?? "Connected")
213+
.font(.caption2)
214+
.foregroundColor(authManager.qwenStatus.isExpired ? .red : .green)
215+
if authManager.qwenStatus.isExpired {
216+
Text("(expired)")
217+
.font(.caption2)
218+
.foregroundColor(.red)
219+
}
220+
}
221+
}
222+
Spacer()
223+
if isAuthenticatingQwen {
224+
ProgressView()
225+
.controlSize(.small)
226+
} else {
227+
if authManager.qwenStatus.isAuthenticated {
228+
if authManager.qwenStatus.isExpired {
229+
Button("Reconnect") {
230+
connectQwen()
231+
}
232+
} else {
233+
Button("Disconnect") {
234+
disconnectQwen()
235+
}
236+
}
237+
} else {
238+
Button("Connect") {
239+
connectQwen()
240+
}
241+
}
242+
}
243+
}
198244
}
199245
}
200246
.formStyle(.grouped)
@@ -260,7 +306,33 @@ struct SettingsView: View {
260306
}
261307
.padding(.bottom, 12)
262308
}
263-
.frame(width: 480, height: 440)
309+
.frame(width: 480, height: 490)
310+
.sheet(isPresented: $showingQwenEmailPrompt) {
311+
VStack(spacing: 16) {
312+
Text("Qwen Account Email")
313+
.font(.headline)
314+
Text("Enter your Qwen account email address")
315+
.font(.caption)
316+
.foregroundColor(.secondary)
317+
TextField("[email protected]", text: $qwenEmail)
318+
.textFieldStyle(.roundedBorder)
319+
.frame(width: 250)
320+
HStack(spacing: 12) {
321+
Button("Cancel") {
322+
showingQwenEmailPrompt = false
323+
qwenEmail = ""
324+
}
325+
Button("Continue") {
326+
showingQwenEmailPrompt = false
327+
startQwenAuth(email: qwenEmail)
328+
}
329+
.disabled(qwenEmail.isEmpty)
330+
.keyboardShortcut(.defaultAction)
331+
}
332+
}
333+
.padding(24)
334+
.frame(width: 350)
335+
}
264336
.onAppear {
265337
authManager.checkAuthStatus()
266338
checkLaunchAtLogin()
@@ -400,6 +472,43 @@ struct SettingsView: View {
400472
}
401473
}
402474

475+
private func connectQwen() {
476+
showingQwenEmailPrompt = true
477+
}
478+
479+
private func startQwenAuth(email: String) {
480+
isAuthenticatingQwen = true
481+
NSLog("[SettingsView] Starting Qwen authentication with email: %@", email)
482+
483+
serverManager.runAuthCommand(.qwenLogin(email: email)) { success, output in
484+
NSLog("[SettingsView] Auth completed - success: %d, output: %@", success, output)
485+
DispatchQueue.main.async {
486+
self.isAuthenticatingQwen = false
487+
488+
if success {
489+
self.authResultSuccess = true
490+
self.authResultMessage = "✓ Qwen authenticated successfully!\n\nPlease complete the authentication in your browser, then the app will automatically submit your email and detect your credentials."
491+
self.showingAuthResult = true
492+
// File monitor will automatically update the status
493+
} else {
494+
self.authResultSuccess = false
495+
self.authResultMessage = "Authentication failed. Please check if the browser opened and try again.\n\nDetails: \(output.isEmpty ? "No output from authentication process" : output)"
496+
self.showingAuthResult = true
497+
}
498+
}
499+
}
500+
}
501+
502+
private func disconnectQwen() {
503+
isAuthenticatingQwen = true
504+
performDisconnect(for: "qwen", serviceName: "Qwen") { success, message in
505+
self.isAuthenticatingQwen = false
506+
self.authResultSuccess = success
507+
self.authResultMessage = message
508+
self.showingAuthResult = true
509+
}
510+
}
511+
403512
private func startMonitoringAuthDirectory() {
404513
let authDir = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".cli-proxy-api")
405514

0 commit comments

Comments
 (0)