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:
Roman
2026-05-20 15:07:42 +04:00
Unverified
parent ea60d74527
commit ea18535f1c
2 changed files with 113 additions and 47 deletions

Binary file not shown.

View File

@@ -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'>&#10003; 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">&#10003; 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()