v2.3.2: fix Add/Edit dialog crash, smarter Google OAuth UX
- Fix missing _on_reasoning_toggled method (caused Add button crash) - Redesigned Google OAuth flow with proper dialog: - Shows clickable auth URL link in dialog - Auto-opens browser for Google authorization - Live status updates while waiting for callback - Success/error shown in dialog (no popup chain) - Spinner animation during auth wait - Better setup instructions if client_secret.json missing
This commit is contained in:
@@ -1645,6 +1645,14 @@ class EditEndpointDialog(Gtk.Dialog):
|
||||
if initial and self._data.get("models"):
|
||||
self._refresh_default_combo(self._data.get("default_model", ""))
|
||||
|
||||
def _on_reasoning_toggled(self, *_):
|
||||
active = self._switch_reasoning.get_active()
|
||||
self._combo_effort.set_sensitive(active)
|
||||
if active:
|
||||
self._lbl_reasoning.set_markup('<span foreground="#27ae60" weight="bold">ON</span>')
|
||||
else:
|
||||
self._lbl_reasoning.set_markup('<span foreground="#e67e22" weight="bold">OFF</span>')
|
||||
|
||||
def _do_oauth_login(self):
|
||||
preset_name = self._combo_preset.get_active_text() or "Custom"
|
||||
preset = PROVIDER_PRESETS.get(preset_name, {})
|
||||
@@ -1657,21 +1665,36 @@ class EditEndpointDialog(Gtk.Dialog):
|
||||
client_secret_path = os.path.expanduser("~/.cache/codex-proxy/client_secret.json")
|
||||
|
||||
if not os.path.exists(client_secret_path):
|
||||
d = Gtk.MessageDialog(self, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK,
|
||||
"Google OAuth Setup\n\n"
|
||||
"1. Go to https://console.cloud.google.com/\n"
|
||||
"2. Create a project → Enable 'Generative Language API'\n"
|
||||
"3. Go to APIs & Services → Credentials → Create OAuth 2.0 Client ID\n"
|
||||
"4. Application type: Desktop app\n"
|
||||
"5. Add redirect URI: http://localhost:8085\n"
|
||||
"6. Download client_secret.json\n"
|
||||
f"7. Save it to: {client_secret_path}\n\n"
|
||||
"Then click OAuth Login again.")
|
||||
d.run(); d.destroy()
|
||||
dlg = Gtk.Dialog(title="Google OAuth Setup", parent=self, modal=True)
|
||||
dlg.add_button("Open Google Console", 1)
|
||||
dlg.add_button("I have client_secret.json", 2)
|
||||
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
||||
dlg.set_default_size(500, 320)
|
||||
area = dlg.get_content_area()
|
||||
area.set_margin_start(16)
|
||||
area.set_margin_end(16)
|
||||
area.set_margin_top(12)
|
||||
area.set_margin_bottom(12)
|
||||
area.set_spacing(8)
|
||||
area.pack_start(Gtk.Label(label="<b>One-time Google OAuth Setup</b>", use_markup=True), False, False, 0)
|
||||
steps = Gtk.Label(label=(
|
||||
"1. Create project at Google Cloud Console\n"
|
||||
" (enable 'Generative Language API')\n"
|
||||
"2. APIs & Services → Credentials\n"
|
||||
" → Create OAuth 2.0 Client ID (Desktop app)\n"
|
||||
"3. Add redirect URI: http://localhost:8085\n"
|
||||
"4. Download client_secret.json\n"
|
||||
f"5. Save to: {client_secret_path}\n\n"
|
||||
"Then click 'I have client_secret.json'"
|
||||
), xalign=0)
|
||||
area.pack_start(steps, False, False, 4)
|
||||
area.show_all()
|
||||
r = dlg.run()
|
||||
dlg.destroy()
|
||||
if r == 1:
|
||||
subprocess.Popen(["xdg-open", "https://console.cloud.google.com/apis/credentials"])
|
||||
return
|
||||
|
||||
import http.server, threading, urllib.parse, json, urllib.request
|
||||
|
||||
with open(client_secret_path) as f:
|
||||
cs = json.load(f)
|
||||
installed = cs.get("installed", cs.get("web", {}))
|
||||
@@ -1686,7 +1709,36 @@ class EditEndpointDialog(Gtk.Dialog):
|
||||
f"&response_type=code&scope={urllib.parse.quote(scope)}&access_type=offline&prompt=consent"
|
||||
)
|
||||
|
||||
dlg = Gtk.Dialog(title="Google OAuth", parent=self, modal=True)
|
||||
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
|
||||
dlg.set_default_size(520, 260)
|
||||
area = dlg.get_content_area()
|
||||
area.set_margin_start(16)
|
||||
area.set_margin_end(16)
|
||||
area.set_margin_top(12)
|
||||
area.set_margin_bottom(12)
|
||||
area.set_spacing(8)
|
||||
|
||||
area.pack_start(Gtk.Label(label="<b>Step 1: Authorize with Google</b>", use_markup=True, xalign=0), False, False, 0)
|
||||
link_lbl = Gtk.Label(label=f'<a href="{auth_url}">Click here to open Google authorization</a>', use_markup=True)
|
||||
area.pack_start(link_lbl, False, False, 4)
|
||||
|
||||
area.pack_start(Gtk.Label(label="Opening browser automatically…", xalign=0), False, False, 0)
|
||||
|
||||
area.pack_start(Gtk.Label(label="<b>Waiting for authorization…</b>", use_markup=True, xalign=0), False, False, 12)
|
||||
self._oauth_status = Gtk.Label(label="Listening on http://localhost:8085 …", xalign=0)
|
||||
area.pack_start(self._oauth_status, False, False, 0)
|
||||
|
||||
spinner = Gtk.Spinner()
|
||||
spinner.start()
|
||||
area.pack_start(spinner, False, False, 8)
|
||||
|
||||
area.show_all()
|
||||
|
||||
import http.server
|
||||
code_holder = [None]
|
||||
error_holder = [None]
|
||||
|
||||
class OAuthHandler(http.server.BaseHTTPRequestHandler):
|
||||
def do_GET(self2):
|
||||
qs = urllib.parse.urlparse(self2.path).query
|
||||
@@ -1696,37 +1748,59 @@ class EditEndpointDialog(Gtk.Dialog):
|
||||
self2.send_response(200)
|
||||
self2.send_header("Content-Type", "text/html")
|
||||
self2.end_headers()
|
||||
self2.wfile.write(b"<h1>Authorization successful! You can close this tab.</h1>")
|
||||
self2.wfile.write(b"<html><body style='font-family:sans-serif;text-align:center;padding-top:80px'>"
|
||||
b"<h2 style='color:#27ae60'>✓ Authorization successful!</h2>"
|
||||
b"<p>You can close this tab and return to Codex Launcher.</p></body></html>")
|
||||
else:
|
||||
error_holder[0] = params.get("error", ["unknown"])[0]
|
||||
self2.send_response(400)
|
||||
self2.send_header("Content-Type", "text/html")
|
||||
self2.end_headers()
|
||||
self2.wfile.write(b"Authorization failed.")
|
||||
self2.wfile.write(b"<html><body style='font-family:sans-serif;text-align:center;padding-top:80px'>"
|
||||
b"<h2 style='color:#e74c3c'>Authorization failed.</h2>"
|
||||
b"<p>Please close this tab and try again.</p></body></html>")
|
||||
def log_message(self2, *a): pass
|
||||
|
||||
server = http.server.HTTPServer(("127.0.0.1", 8085), OAuthHandler)
|
||||
t = threading.Thread(target=server.handle_request, daemon=True)
|
||||
t.start()
|
||||
|
||||
os.system(f"xdg-open '{auth_url}' 2>/dev/null &")
|
||||
|
||||
t.join(timeout=120)
|
||||
server.server_close()
|
||||
|
||||
if not code_holder[0]:
|
||||
d = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, "OAuth timed out or failed.")
|
||||
d.run(); d.destroy()
|
||||
try:
|
||||
server = http.server.HTTPServer(("127.0.0.1", 8085), OAuthHandler)
|
||||
except OSError:
|
||||
self._oauth_status.set_text("Port 8085 already in use — close other apps and retry.")
|
||||
spinner.stop()
|
||||
dlg.run(); dlg.destroy()
|
||||
return
|
||||
|
||||
token_data = urllib.parse.urlencode({
|
||||
"code": code_holder[0],
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"redirect_uri": redirect_uri,
|
||||
"grant_type": "authorization_code",
|
||||
}).encode()
|
||||
req = urllib.request.Request("https://oauth2.googleapis.com/token", data=token_data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"})
|
||||
def wait_for_code():
|
||||
server.handle_request()
|
||||
server.server_close()
|
||||
GLib.idle_add(self._google_oauth_complete, dlg, code_holder, error_holder,
|
||||
client_id, client_secret, redirect_uri, token_path, spinner)
|
||||
|
||||
threading.Thread(target=wait_for_code, daemon=True).start()
|
||||
subprocess.Popen(["xdg-open", auth_url], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
dlg.run()
|
||||
dlg.destroy()
|
||||
|
||||
def _google_oauth_complete(self, dlg, code_holder, error_holder,
|
||||
client_id, client_secret, redirect_uri, token_path, spinner):
|
||||
spinner.stop()
|
||||
if error_holder[0]:
|
||||
self._oauth_status.set_markup(f'<span foreground="#e74c3c">Error: {error_holder[0]}</span>')
|
||||
return
|
||||
if not code_holder[0]:
|
||||
self._oauth_status.set_text("No authorization code received.")
|
||||
return
|
||||
|
||||
self._oauth_status.set_text("Exchanging code for token…")
|
||||
try:
|
||||
token_data = urllib.parse.urlencode({
|
||||
"code": code_holder[0],
|
||||
"client_id": client_id,
|
||||
"client_secret": client_secret,
|
||||
"redirect_uri": redirect_uri,
|
||||
"grant_type": "authorization_code",
|
||||
}).encode()
|
||||
req = urllib.request.Request("https://oauth2.googleapis.com/token", data=token_data,
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"})
|
||||
resp = urllib.request.urlopen(req, timeout=30)
|
||||
tokens = json.loads(resp.read())
|
||||
tokens["client_id"] = client_id
|
||||
@@ -1735,18 +1809,10 @@ class EditEndpointDialog(Gtk.Dialog):
|
||||
with open(token_path, "w") as f:
|
||||
json.dump(tokens, f, indent=2)
|
||||
self._entry_key.set_text(tokens.get("access_token", ""))
|
||||
d = Gtk.MessageDialog(self, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK,
|
||||
f"Google OAuth successful!\n\nAccess token saved.\nIt will auto-refresh when expired.")
|
||||
d.run(); d.destroy()
|
||||
self._oauth_status.set_markup('<span foreground="#27ae60" weight="bold">✓ Authorization successful! Token saved.</span>')
|
||||
dlg.set_title("Google OAuth — Success")
|
||||
except Exception as e:
|
||||
d = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, f"Token exchange failed:\n{e}")
|
||||
d.run(); d.destroy()
|
||||
active = self._switch_reasoning.get_active()
|
||||
self._combo_effort.set_sensitive(active)
|
||||
if active:
|
||||
self._lbl_reasoning.set_markup('<span foreground="#27ae60" weight="bold">ON</span>')
|
||||
else:
|
||||
self._lbl_reasoning.set_markup('<span foreground="#e67e22" weight="bold">OFF</span>')
|
||||
self._oauth_status.set_markup(f'<span foreground="#e74c3c">Token exchange failed: {e}</span>')
|
||||
|
||||
def _remove_model(self, path):
|
||||
current = self._combo_default.get_active_text()
|
||||
|
||||
Reference in New Issue
Block a user