diff --git a/src/codex-launcher-gui b/src/codex-launcher-gui
index e0b8292..dfe59b4 100755
--- a/src/codex-launcher-gui
+++ b/src/codex-launcher-gui
@@ -26,6 +26,10 @@ model_catalog_json = ""
"""
CHANGELOG = [
+ ("3.10.4", "2026-05-25", [
+ "OAuth Secrets editor in GUI — update client ID/secret without editing files",
+ "Secrets stored in ~/.config/codex-launcher/oauth-secrets.json (not in repo)",
+ ]),
("3.10.3", "2026-05-25", [
"Fix Antigravity 404: map display names to verified REST API model IDs",
"REST API uses slugs (gemini-3-flash) not display names (Gemini 3.5 Flash)",
@@ -1776,7 +1780,7 @@ class LauncherWin(Gtk.Window):
# header row
hdr = Gtk.Box(spacing=8)
vbox.pack_start(hdr, False, False, 0)
- lbl = Gtk.Label(label="Codex Launcher v3.10.3")
+ lbl = Gtk.Label(label="Codex Launcher v3.10.4")
lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0)
changelog_btn = Gtk.Button(label="Changelog")
@@ -1800,6 +1804,9 @@ class LauncherWin(Gtk.Window):
mgr_btn = Gtk.Button(label="Manage Endpoints")
mgr_btn.connect("clicked", lambda b: self._open_mgr())
hdr.pack_end(mgr_btn, False, False, 0)
+ oauth_btn = Gtk.Button(label="OAuth Secrets")
+ oauth_btn.connect("clicked", lambda b: self._edit_oauth_secrets())
+ hdr.pack_end(oauth_btn, False, False, 0)
# verification status bar
self._cli_info = _detect_codex_cli()
@@ -3260,6 +3267,64 @@ class EditEndpointDialog(Gtk.Dialog):
else:
self._lbl_reasoning.set_markup('OFF')
+ def _edit_oauth_secrets(self):
+ secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
+ try:
+ with open(secrets_path) as f:
+ data = json.load(f)
+ except Exception:
+ data = {"antigravity": {"client_id": "", "client_secret": ""},
+ "gemini_cli": {"client_id": "", "client_secret": ""}}
+
+ dlg = Gtk.Dialog(title="OAuth 2.0 Client Secrets", parent=self, modal=True)
+ dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
+ dlg.add_button("Save", Gtk.ResponseType.OK)
+ dlg.set_default_size(520, 340)
+ 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(6)
+
+ area.pack_start(Gtk.Label(label="Google OAuth 2.0 credentials\nStored locally in ~/.config/codex-launcher/oauth-secrets.json", use_markup=True, xalign=0), False, False, 4)
+
+ fields = {}
+ for section_key, section_label in [("antigravity", "Antigravity (CloudCode)"), ("gemini_cli", "Gemini CLI")]:
+ area.pack_start(Gtk.Label(label=f"\n{section_label}", use_markup=True, xalign=0), False, False, 2)
+ sec = data.get(section_key, {})
+ for fk, fl in [("client_id", "Client ID"), ("client_secret", "Client Secret")]:
+ row = Gtk.Box(spacing=6)
+ lbl = Gtk.Label(label=fl + ":", xalign=0)
+ lbl.set_size_request(100, -1)
+ entry = Gtk.Entry()
+ entry.set_text(sec.get(fk, ""))
+ entry.set_size_request(380, -1)
+ if fk == "client_secret":
+ entry.set_visibility(False)
+ entry.set_invisible_char("*")
+ row.pack_start(lbl, False, False, 0)
+ row.pack_start(entry, True, True, 0)
+ area.pack_start(row, False, False, 2)
+ fields[(section_key, fk)] = entry
+
+ area.pack_start(Gtk.Label(label="\nGet credentials from console.cloud.google.com → APIs & Services → Credentials", use_markup=True, xalign=0), False, False, 4)
+ area.show_all()
+
+ if dlg.run() == Gtk.ResponseType.OK:
+ for (sk, fk), entry in fields.items():
+ if sk not in data:
+ data[sk] = {}
+ data[sk][fk] = entry.get_text().strip()
+ try:
+ os.makedirs(os.path.dirname(secrets_path), exist_ok=True)
+ with open(secrets_path, "w") as f:
+ json.dump(data, f, indent=2)
+ os.chmod(secrets_path, 0o600)
+ except Exception as e:
+ self._show_error_dialog("Save failed", str(e))
+ dlg.destroy()
+
def _do_oauth_login(self):
preset_name = self._combo_preset.get_active_text() or "Custom"
preset = PROVIDER_PRESETS.get(preset_name, {})
@@ -3589,7 +3654,7 @@ class EditEndpointDialog(Gtk.Dialog):
auth_url = "https://codebuff.com/api/auth/cli/code"
body = json.dumps({"fingerprintId": fingerprint_id}).encode()
req = urllib.request.Request(auth_url, data=body,
- headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.3"})
+ headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.4"})
resp = urllib.request.urlopen(req, timeout=30)
data = json.loads(resp.read())
login_url = data.get("loginUrl", "") or data.get("login_url", "")
@@ -3614,7 +3679,7 @@ class EditEndpointDialog(Gtk.Dialog):
time.sleep(2)
try:
poll_req = urllib.request.Request(poll_url,
- headers={"User-Agent": "codex-launcher/3.10.3"})
+ headers={"User-Agent": "codex-launcher/3.10.4"})
poll_resp = urllib.request.urlopen(poll_req, timeout=10)
poll_data = json.loads(poll_resp.read())
user = poll_data.get("user")
diff --git a/src/translate-proxy.py b/src/translate-proxy.py
index ab37c02..ec5d06d 100755
--- a/src/translate-proxy.py
+++ b/src/translate-proxy.py
@@ -335,7 +335,7 @@ def _codebuff_get_session(token, model):
req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
- "User-Agent": "codex-launcher/3.10.3",
+ "User-Agent": "codex-launcher/3.10.4",
"x-codebuff-model": model,
})
try:
@@ -383,7 +383,7 @@ def _codebuff_start_run(token, agent_id):
req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
- "User-Agent": "codex-launcher/3.10.3",
+ "User-Agent": "codex-launcher/3.10.4",
})
try:
resp = urllib.request.urlopen(req, timeout=15)
@@ -416,7 +416,7 @@ def _codebuff_finish_run(token, run_id, status="completed"):
req = urllib.request.Request(url, data=body, headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
- "User-Agent": "codex-launcher/3.10.3",
+ "User-Agent": "codex-launcher/3.10.4",
})
try:
urllib.request.urlopen(req, timeout=10)
@@ -5342,7 +5342,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
- "User-Agent": "codex-launcher/3.10.3",
+ "User-Agent": "codex-launcher/3.10.4",
"x-codebuff-model": model,
}
if instance_id:
@@ -5508,7 +5508,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
if body.get("tool_choice"):
chat_body["tool_choice"] = body["tool_choice"]
target = f"{_CODEBUFF_API_URL}/api/v1/chat/completions"
- headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "User-Agent": "codex-launcher/3.10.3", "x-codebuff-model": model}
+ headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "User-Agent": "codex-launcher/3.10.4", "x-codebuff-model": model}
if instance_id:
headers["x-codebuff-instance-id"] = instance_id
print(f"[codebuff] retry POST {target} model={model} stream={stream} run={run_id} (thinking disabled via DeepSeek native)", file=sys.stderr)