v3.10.6: Linux/Windows feature parity — OAuth Secrets all providers, Re-OAuth, Sync from Preset, Codebuff OAuth
This commit is contained in:
@@ -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("<Button-1>", 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("<Button-1>", 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("<Configure>", 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)
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user