diff --git a/CHANGELOG.md b/CHANGELOG.md index 0607d02..c17a5d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,11 @@ - Adaptive compact budget raised from 60% to 80% — avoids premature compaction on large-context models (DeepSeek v4 Pro 1M) - Config cleanup fix: stale `proxy-*.json` cleanup moved after `_init_runtime()` to avoid deleting active config - Windows GUI: added Clear Log, Restart Proxy, View Log buttons +- **Linux/Windows feature parity**: both GUIs now have identical features +- Windows GUI: ported OAuth Secrets all-providers dialog (Google + Freebuff/Codebuff with Re-OAuth buttons, token status) +- Windows GUI: added Codebuff/Freebuff OAuth login flow (GitHub browser-based) +- Windows GUI: added Sync from Preset button in endpoint editor +- Linux GUI: added Clear Log + Restart Proxy buttons (matching Windows) ## v3.10.5 (2026-05-25) diff --git a/README.md b/README.md index 1317250..fb31a47 100644 --- a/README.md +++ b/README.md @@ -789,7 +789,7 @@ codex --profile my-profile -c model=my-model ## Windows Version -A native **Windows GUI** (tkinter) is available in the [`windows/`](./windows/) folder, ported from the Linux GTK version. +A native **Windows GUI** (tkinter) is available in the `src/` folder alongside the Linux version. Both GUIs have **full feature parity**.

@@ -802,8 +802,10 @@ A native **Windows GUI** (tkinter) is available in the [`windows/`](./windows/) | File | Purpose | |---|---| -| `windows/codex-launcher-gui.py` | tkinter GUI — manage endpoints, launch Codex CLI/Desktop | -| `windows/codex_launcher_lib.py` | Shared library — proxy lifecycle, config, OAuth, diagnostics | +| `src/codex-launcher-gui.py` | tkinter GUI (Windows) — manage endpoints, launch Codex CLI/Desktop | +| `src/codex-launcher-gui` | GTK GUI (Linux) — same features, native GTK look | +| `src/codex_launcher_lib.py` | Shared library — proxy lifecycle, config, OAuth, diagnostics | +| `src/translate-proxy.py` | Proxy — translates Responses API for any provider | ### How to Run (Windows) @@ -811,7 +813,7 @@ Python ≥ 3.8 with tkinter is required (comes with the official Python installe ```powershell # From repo root -cd windows +cd src python codex-launcher-gui.py ``` @@ -824,14 +826,18 @@ The GUI will: Google OAuth (Antigravity / Gemini CLI) requires a `client_secret_*.json` from [Google Cloud Console](https://console.cloud.google.com/apis/credentials). Use the **OAuth Secrets** button in the GUI to import it — credentials are stored locally in `~/.config/codex-launcher/oauth-secrets.json`, never in the repo. -### Shared Backend +The **OAuth Secrets** dialog shows all providers (Google + Freebuff/Codebuff) with **Re-OAuth buttons** to instantly re-authenticate any provider. -The same `translate-proxy.py` powers both Linux and Windows. All fixes apply to both: -- Antigravity REST model ID mapping -- Context compaction (60% of token limit) -- Multi-account rotation -- Rate limit handling -- AI Monitoring / self-revive watchdog +### Feature Parity + +Both Linux (GTK) and Windows (tkinter) GUIs have identical features: +- All provider presets, endpoint management, BGP routing +- OAuth Secrets with all providers + Re-OAuth buttons +- AI Monitor, Usage Dashboard, Request History, Benchmark +- Clear Log, Restart Proxy, View Log +- Doctor, Diagnostic Agent, Profile Backup/Import +- Antigravity model mapping, context compaction (80% budget) +- Multi-account rotation, rate limit handling --- diff --git a/codex-launcher_3.10.6_all.deb b/codex-launcher_3.10.6_all.deb index 50eaf0f..55385e2 100644 Binary files a/codex-launcher_3.10.6_all.deb and b/codex-launcher_3.10.6_all.deb differ diff --git a/src/codex-launcher-gui.py b/src/codex-launcher-gui.py index 8045bb1..286e35e 100644 --- a/src/codex-launcher-gui.py +++ b/src/codex-launcher-gui.py @@ -236,6 +236,7 @@ class EditEndpointDialog: ttk.Button(model_input_frame, text="Add", command=self._add_model).pack(side="left", padx=(4, 0)) ttk.Button(model_input_frame, text="Bulk Add", command=self._add_models_from_text).pack(side="left", padx=(4, 0)) ttk.Button(model_input_frame, text="Fetch from API", command=self._fetch_models).pack(side="left", padx=(4, 0)) + ttk.Button(model_input_frame, text="Sync from Preset", command=lambda: self._apply_selected_preset_force()).pack(side="left", padx=(4, 0)) ttk.Button(model_input_frame, text="Test Endpoint", command=self._diagnose_endpoint).pack(side="left", padx=(4, 0)) ttk.Label(main, text="Bulk add (one per line or comma-separated):").pack(anchor="w", pady=(4, 0)) @@ -298,6 +299,26 @@ class EditEndpointDialog: if preset["models"]: self._combo_default.set(preset["models"][0]) + def _apply_selected_preset_force(self): + preset_name = self._combo_preset.get() or "Custom" + preset = PROVIDER_PRESETS.get(preset_name, {}) + bt = preset.get("backend_type", "openai-compat") + bt_display = next((k for k, v in self._bt_map.items() if v == bt), list(self._bt_map.keys())[0]) + self._combo_type.set(bt_display) + self._entry_url.delete(0, "end") + self._entry_url.insert(0, preset.get("base_url", "")) + cc_ver = preset.get("cc_version", "") + if cc_ver: + self._entry_cc_ver.delete(0, "end") + self._entry_cc_ver.insert(0, cc_ver) + if preset.get("models"): + self._model_listbox.delete(0, "end") + for mid in preset["models"]: + self._model_listbox.insert("end", mid) + self._refresh_default_combo() + if preset["models"]: + self._combo_default.set(preset["models"][0]) + def _add_model(self): m = normalize_model_id(self._entry_model.get()) if m: @@ -373,7 +394,9 @@ class EditEndpointDialog: preset_name = self._combo_preset.get() or "Custom" preset = PROVIDER_PRESETS.get(preset_name, {}) provider = preset.get("oauth_provider", "") - if (provider or "").startswith("google"): + if provider == "codebuff": + self._codebuff_oauth_flow() + elif (provider or "").startswith("google"): self._google_oauth_flow(provider) def _google_oauth_flow(self, oauth_provider="google-cli"): @@ -564,6 +587,81 @@ class EditEndpointDialog: self._oauth_status_var.set(f"Failed: {msg}") self._dlg.after(3000, dlg.destroy) + def _codebuff_oauth_flow(self): + import uuid + oauth_dlg = tk.Toplevel(self._dlg) + oauth_dlg.title("Codebuff / Freebuff Login") + oauth_dlg.geometry("520x240") + oauth_dlg.transient(self._dlg) + oauth_dlg.grab_set() + tk.Label(oauth_dlg, text="Sign in with GitHub via Codebuff", font=("Segoe UI", 11, "bold")).pack(padx=16, pady=(12, 0), anchor="w") + self._cb_status_var = tk.StringVar(value="Requesting login URL...") + tk.Label(oauth_dlg, textvariable=self._cb_status_var).pack(padx=16, pady=(8, 0), anchor="w") + self._cb_link_lbl = tk.Label(oauth_dlg, text="", fg="blue", cursor="hand2") + self._cb_link_lbl.pack(padx=16, anchor="w") + self._cb_oauth_result = {"success": False, "user": None, "error": None} + self._cb_oauth_dlg = oauth_dlg + + def _thread(): + try: + fp_id = str(uuid.uuid4()) + body = json.dumps({"fingerprintId": fp_id}).encode() + req = urllib.request.Request("https://www.codebuff.com/api/auth/cli/code", + data=body, headers={"Content-Type": "application/json", "User-Agent": UA}) + resp = urllib.request.urlopen(req, timeout=30) + rdata = json.loads(resp.read()) + login_url = rdata.get("loginUrl", "") or rdata.get("login_url", "") + fp_hash = rdata.get("fingerprintHash", "") or rdata.get("fingerprint_hash", "") + expires_at = rdata.get("expiresAt", 0) or rdata.get("expires_at", 0) + if not login_url: + self._cb_oauth_result["error"] = "No login URL" + self._dlg.after(0, self._codebuff_oauth_done) + return + def _set_link(): + self._cb_status_var.set("Open this URL in your browser to log in:") + self._cb_link_lbl.configure(text=login_url) + self._cb_link_lbl.bind("", lambda e: open_url(login_url)) + self._dlg.after(0, _set_link) + open_url(login_url) + poll = f"https://www.codebuff.com/api/auth/cli/status?fingerprintId={urllib.parse.quote(fp_id)}&fingerprintHash={urllib.parse.quote(fp_hash)}&expiresAt={expires_at}" + deadline = time.time() + 300 + while time.time() < deadline: + time.sleep(2) + try: + pr = urllib.request.Request(poll, headers={"User-Agent": UA}) + pd = json.loads(urllib.request.urlopen(pr, timeout=10).read()) + if pd.get("user", {}).get("authToken"): + self._cb_oauth_result["success"] = True + self._cb_oauth_result["user"] = pd["user"] + self._dlg.after(0, self._codebuff_oauth_done) + return + except Exception: + pass + self._cb_oauth_result["error"] = "Timed out" + except Exception as e: + self._cb_oauth_result["error"] = str(e)[:200] + self._dlg.after(0, self._codebuff_oauth_done) + + threading.Thread(target=_thread, daemon=True).start() + + def _codebuff_oauth_done(self): + if self._cb_oauth_result["success"] and self._cb_oauth_result["user"]: + u = self._cb_oauth_result["user"] + cb_creds_path = str(HOME / ".config" / "manicode" / "credentials.json") + os.makedirs(os.path.dirname(cb_creds_path), exist_ok=True) + creds = {"default": {"id": u.get("id", ""), "name": u.get("name", ""), + "email": u.get("email", ""), "authToken": u.get("authToken", ""), + "fingerprintId": u.get("fingerprintId", ""), "fingerprintHash": u.get("fingerprintHash", "")}} + with open(cb_creds_path, "w") as f: + json.dump(creds, f, indent=2) + self._cb_status_var.set(f"Logged in as {u.get('email', 'OK')}") + self._cb_link_lbl.configure(text="") + self._entry_key.delete(0, "end") + self._entry_key.insert(0, u.get("authToken", "")) + self._dlg.after(2000, self._cb_oauth_dlg.destroy) + else: + self._cb_status_var.set(f"Failed: {self._cb_oauth_result.get('error', 'unknown')}") + def _cancel(self): self._dlg.destroy() @@ -2230,40 +2328,187 @@ class LauncherWin: if Path(assist_path).exists(): subprocess.Popen([sys.executable, assist_path], creationflags=subprocess.CREATE_NEW_PROCESS_GROUP if IS_WINDOWS else 0) + def _google_reoauth(self, provider, parent_dlg=None): + is_antigravity = provider == "google-antigravity" + sec_key = "antigravity" if is_antigravity else "gemini_cli" + secrets = load_oauth_secrets() + sec = secrets.get(sec_key, {}) + client_id = sec.get("client_id", "") + client_secret = sec.get("client_secret", "") + if not client_id or not client_secret: + messagebox.showerror("Missing OAuth secrets", + f"No client_id/client_secret for {sec_key}.\nSet them in OAuth Secrets first.") + return + token_file = "google-antigravity-oauth-token.json" if is_antigravity else "google-cli-oauth-token.json" + token_path = str(PROXY_CONFIG_DIR / token_file) + redirect = "urn:ietf:wg:oauth:2.0:oob" + scope_str = "https://www.googleapis.com/auth/cloud-platform" + auth_url = (f"https://accounts.google.com/o/oauth2/v2/auth?client_id={client_id}" + f"&redirect_uri={urllib.parse.quote(redirect)}" + f"&response_type=code&scope={urllib.parse.quote(scope_str)}" + f"&access_type=offline&prompt=consent") + open_url(auth_url) + code = tk.simpledialog.askstring("Re-OAuth", + f"Paste the authorization code for {'Antigravity' if is_antigravity else 'Gemini CLI'}:", + parent=parent_dlg or self._root) + if not code: + return + try: + tok_req = urllib.request.Request("https://oauth2.googleapis.com/token", + data=urllib.parse.urlencode({ + "code": code, "client_id": client_id, "client_secret": client_secret, + "redirect_uri": redirect, "grant_type": "authorization_code" + }).encode(), + headers={"Content-Type": "application/x-www-form-urlencoded"}) + tok_resp = urllib.request.urlopen(tok_req, timeout=30) + tok_data = json.loads(tok_resp.read()) + tok_data["_updated"] = time.time() + tok_data["client_id"] = client_id + tok_data["client_secret"] = client_secret + tok_data["provider_kind"] = "antigravity" if is_antigravity else "cli" + os.makedirs(os.path.dirname(token_path), exist_ok=True) + with open(token_path, "w") as f: + json.dump(tok_data, f, indent=2) + self.log(f"[oauth] Refreshed {provider} token") + except Exception as e: + messagebox.showerror("Token exchange failed", str(e)[:300]) + + def _codebuff_reoauth_standalone(self, parent_dlg=None): + import uuid + oauth_dlg = tk.Toplevel(parent_dlg or self._root) + oauth_dlg.title("Freebuff / Codebuff Login") + oauth_dlg.geometry("520x240") + if parent_dlg: + oauth_dlg.transient(parent_dlg) + else: + oauth_dlg.transient(self._root) + oauth_dlg.grab_set() + tk.Label(oauth_dlg, text="Sign in with GitHub via Codebuff", font=("Segoe UI", 11, "bold")).pack(padx=16, pady=(12, 0), anchor="w") + status_var = tk.StringVar(value="Requesting login URL...") + tk.Label(oauth_dlg, textvariable=status_var).pack(padx=16, pady=(8, 0), anchor="w") + link_lbl = tk.Label(oauth_dlg, text="", fg="blue", cursor="hand2") + link_lbl.pack(padx=16, anchor="w") + result = {"success": False, "user": None, "error": None} + + def _thread(): + try: + fp_id = str(uuid.uuid4()) + body = json.dumps({"fingerprintId": fp_id}).encode() + req = urllib.request.Request("https://www.codebuff.com/api/auth/cli/code", + data=body, headers={"Content-Type": "application/json", "User-Agent": UA}) + resp = urllib.request.urlopen(req, timeout=30) + rdata = json.loads(resp.read()) + login_url = rdata.get("loginUrl", "") or rdata.get("login_url", "") + fp_hash = rdata.get("fingerprintHash", "") or rdata.get("fingerprint_hash", "") + expires_at = rdata.get("expiresAt", 0) or rdata.get("expires_at", 0) + if not login_url: + result["error"] = "No login URL" + self._root.after(0, _done) + return + def _set(): + status_var.set("Open this URL in your browser to log in:") + link_lbl.configure(text=login_url) + link_lbl.bind("", lambda e: open_url(login_url)) + self._root.after(0, _set) + open_url(login_url) + poll = f"https://www.codebuff.com/api/auth/cli/status?fingerprintId={urllib.parse.quote(fp_id)}&fingerprintHash={urllib.parse.quote(fp_hash)}&expiresAt={expires_at}" + deadline = time.time() + 300 + while time.time() < deadline: + time.sleep(2) + try: + pr = urllib.request.Request(poll, headers={"User-Agent": UA}) + pd = json.loads(urllib.request.urlopen(pr, timeout=10).read()) + if pd.get("user", {}).get("authToken"): + result["success"] = True + result["user"] = pd["user"] + self._root.after(0, _done) + return + except Exception: + pass + result["error"] = "Timed out" + except Exception as e: + result["error"] = str(e)[:200] + self._root.after(0, _done) + + def _done(): + if result["success"] and result["user"]: + u = result["user"] + cb_creds_path = str(HOME / ".config" / "manicode" / "credentials.json") + os.makedirs(os.path.dirname(cb_creds_path), exist_ok=True) + creds = {"default": {"id": u.get("id", ""), "name": u.get("name", ""), + "email": u.get("email", ""), "authToken": u.get("authToken", ""), + "fingerprintId": u.get("fingerprintId", ""), "fingerprintHash": u.get("fingerprintHash", "")}} + with open(cb_creds_path, "w") as f: + json.dump(creds, f, indent=2) + status_var.set(f"Logged in as {u.get('email', 'OK')}") + link_lbl.configure(text="") + self._root.after(2000, oauth_dlg.destroy) + else: + status_var.set(f"Failed: {result.get('error', 'unknown')}") + + threading.Thread(target=_thread, daemon=True).start() + oauth_dlg.wait_window() + def _edit_oauth_secrets(self): + import tkinter.simpledialog data = load_oauth_secrets() if not data: data = {"antigravity": {"client_id": "", "client_secret": ""}, "gemini_cli": {"client_id": "", "client_secret": ""}} dlg = tk.Toplevel(self._root) - dlg.title("OAuth 2.0 Client Secrets") - dlg.geometry("600x450") + dlg.title("OAuth Secrets & Credentials") + dlg.geometry("620x650") dlg.transient(self._root) dlg.grab_set() - frame = ttk.Frame(dlg, padding=16) - frame.pack(fill="both", expand=True) + canvas = tk.Canvas(dlg) + scrollbar = ttk.Scrollbar(dlg, orient="vertical", command=canvas.yview) + frame = ttk.Frame(canvas, padding=16) + frame.bind("", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) + canvas.create_window((0, 0), window=frame, anchor="nw") + canvas.configure(yscrollcommand=scrollbar.set) + canvas.pack(side="left", fill="both", expand=True) + scrollbar.pack(side="right", fill="y") - ttk.Label(frame, text="Google OAuth 2.0 credentials", font=("Segoe UI", 10, "bold")).pack(anchor="w") - ttk.Label(frame, text=f"Stored locally in {OAUTH_SECRETS_PATH}", foreground="gray").pack(anchor="w", pady=(0, 8)) + ttk.Label(frame, text="Google OAuth 2.0 Client Credentials", font=("Segoe UI", 10, "bold")).pack(anchor="w") + ttk.Label(frame, text=str(OAUTH_SECRETS_PATH), foreground="gray").pack(anchor="w", pady=(0, 8)) fields = {} nf = ttk.Frame(frame) nf.pack(fill="x") row = 0 - for section_key, section_label in [("antigravity", "Antigravity (CloudCode)"), ("gemini_cli", "Gemini CLI")]: - ttk.Label(nf, text=f"\n{section_label}", font=("Segoe UI", 9, "bold")).grid(row=row, column=0, columnspan=3, sticky="w", pady=(8, 2)) + google_token_dir = str(PROXY_CONFIG_DIR) + for section_key, section_label, oauth_prov, token_file in [ + ("antigravity", "Antigravity (CloudCode)", "google-antigravity", "google-antigravity-oauth-token.json"), + ("gemini_cli", "Gemini CLI", "google-cli", "google-cli-oauth-token.json"), + ]: + ttk.Label(nf, text=f"\n{section_label}", font=("Segoe UI", 9, "bold")).grid(row=row, column=0, columnspan=4, sticky="w", pady=(8, 2)) row += 1 sec = data.get(section_key, {}) + token_path = os.path.join(google_token_dir, token_file) + has_token = False + try: + with open(token_path) as tf: + td = json.load(tf) + has_token = bool(td.get("refresh_token") or td.get("access_token")) + except Exception: + pass + token_status = "Token: valid" if has_token else "Token: missing" + token_color = "#2ea043" if has_token else "#d29922" + ttk.Label(nf, text=token_status, foreground=token_color).grid(row=row, column=0, sticky="w", padx=(8, 4), pady=2) import_btn = ttk.Button(nf, text="Import JSON", command=lambda sk=section_key: self._import_oauth_json(fields, sk)) import_btn.grid(row=row, column=2, padx=(4, 0), pady=2, sticky="e") + reauth_btn = ttk.Button(nf, text="Re-OAuth", + command=lambda p=oauth_prov: self._google_reoauth(p, dlg)) + reauth_btn.grid(row=row, column=3, padx=(4, 0), pady=2, sticky="e") + row += 1 for fk, fl in [("client_id", "Client ID"), ("client_secret", "Client Secret")]: ttk.Label(nf, text=fl + ":").grid(row=row, column=0, sticky="w", padx=(8, 4), pady=2) - entry = ttk.Entry(nf, width=60) + entry = ttk.Entry(nf, width=55) entry.insert(0, sec.get(fk, "")) - entry.grid(row=row, column=1, sticky="ew", pady=2) + entry.grid(row=row, column=1, columnspan=3, sticky="ew", pady=2) if fk == "client_secret": entry.configure(show="*") fields[(section_key, fk)] = entry @@ -2271,7 +2516,50 @@ class LauncherWin: nf.columnconfigure(1, weight=1) - ttk.Label(frame, text="\nImport a client_secret_*.json from Google Cloud Console\nconsole.cloud.google.com → Credentials", foreground="gray").pack(anchor="w") + ttk.Label(frame, text="Import client_secret_*.json from Google Cloud Console → Credentials", foreground="gray").pack(anchor="w") + + ttk.Separator(frame).pack(fill="x", pady=(12, 8)) + + ttk.Label(frame, text="Freebuff / Codebuff Credentials", font=("Segoe UI", 10, "bold")).pack(anchor="w") + ttk.Label(frame, text=str(HOME / ".config" / "manicode" / "credentials.json"), foreground="gray").pack(anchor="w", pady=(0, 8)) + + cb_creds_path = str(HOME / ".config" / "manicode" / "credentials.json") + cb_fields = {} + try: + with open(cb_creds_path) as f: + cb_data = json.load(f) + except Exception: + cb_data = {} + cb_default = cb_data.get("default", {}) + + cb_info = f"Email: {cb_default.get('email', 'not logged in')}" + cb_name = cb_default.get("name", "") + if cb_name: + cb_info = f"{cb_name} — {cb_info}" + has_cb_token = bool(cb_default.get("authToken", "")) + status_text = "Logged in" if has_cb_token else "Not logged in" + status_color = "#2ea043" if has_cb_token else "#d29922" + ttk.Label(frame, text=cb_info).pack(anchor="w") + ttk.Label(frame, text=f"Status: {status_text}", foreground=status_color, font=("Segoe UI", 9, "bold")).pack(anchor="w", pady=(0, 4)) + + cb_nf = ttk.Frame(frame) + cb_nf.pack(fill="x") + cb_row = [0] + for fk, fl in [("authToken", "Auth Token"), ("fingerprintId", "Fingerprint ID")]: + ttk.Label(cb_nf, text=fl + ":").grid(row=cb_row[0], column=0, sticky="w", padx=(8, 4), pady=2) + entry = ttk.Entry(cb_nf, width=55, show="*") + entry.insert(0, cb_default.get(fk, "")) + entry.grid(row=cb_row[0], column=1, sticky="ew", pady=2) + cb_fields[fk] = entry + cb_row[0] += 1 + cb_nf.columnconfigure(1, weight=1) + + ttk.Button(frame, text="Re-OAuth (GitHub Login)", + command=lambda: self._codebuff_reoauth_standalone(dlg)).pack(anchor="w", pady=(4, 0)) + + cb_accounts = cb_data.get("accounts", []) + if cb_accounts: + ttk.Label(frame, text=f"Additional accounts: {len(cb_accounts)} (edit credentials.json manually)", foreground="gray").pack(anchor="w") btnf = ttk.Frame(frame) btnf.pack(fill="x", pady=(12, 0)) @@ -2289,6 +2577,20 @@ class LauncherWin: except Exception as e: messagebox.showerror("Save failed", str(e), parent=dlg) return + cb_updated = dict(cb_default) + for fk, entry in cb_fields.items(): + val = entry.get().strip() + if val: + cb_updated[fk] = val + if cb_updated: + cb_data["default"] = cb_updated + try: + os.makedirs(os.path.dirname(cb_creds_path), exist_ok=True) + with open(cb_creds_path, "w") as f: + json.dump(cb_data, f, indent=2) + except Exception as e: + messagebox.showerror("Save failed", str(e), parent=dlg) + return dlg.destroy() save_btn.configure(command=_save) diff --git a/src/codex_launcher_lib.py b/src/codex_launcher_lib.py index 44387dd..39e4bd2 100644 --- a/src/codex_launcher_lib.py +++ b/src/codex_launcher_lib.py @@ -100,6 +100,9 @@ CHANGELOG = [ "Re-OAuth buttons for each provider: re-authenticate Google or GitHub/Codebuff", "Token status indicators (valid/missing) for each Google provider", "Shows logged-in email and auth status for Freebuff/Codebuff", + "Linux/Windows feature parity: both GUIs have identical features", + "Windows: OAuth Secrets all-providers + Codebuff OAuth + Sync from Preset", + "Linux: Clear Log + Restart Proxy buttons added", ]), ("3.10.5", "2026-05-25", [ "Context compaction for Antigravity/Gemini OAuth — prevents token limit errors",