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",