v3.13.0: Desktop Updater, profile system fix, Antigravity E2E, conservative compaction

- Codex Desktop Updater: check/install/rollback/service management + manual rebuild
- Fix Codex CLI 0.134.0 profiles: separate <slug>.config.toml files
- Fix Antigravity: prod endpoint first, model resolution, OAUTH_PROVIDER
- Fix compaction: max_input_items 60->200 for 1M-token models
- Antigravity E2E test suite: test-antigravity.sh
- Windows GUI: UpdateDesktopWindow + profile slug fix
- Updated CHANGELOG.md and README.md
This commit is contained in:
Roman | RyzenAdvanced
2026-05-27 13:52:02 +04:00
Unverified
parent f9ba3b6e1c
commit 6a50714da6
11 changed files with 10347 additions and 71 deletions

View File

@@ -27,6 +27,15 @@ model_catalog_json = ""
"""
CHANGELOG = [
("3.13.0", "2026-05-27", [
"Codex Desktop Updater: auto-update from ilysenko/codex-desktop-linux",
"Check for updates, install, rollback, service management",
"Manual rebuild from source (clone → build → install .deb)",
"Fix Antigravity: prod endpoint first, skip sandbox service_disabled",
"Fix Antigravity: model resolution (gemini-3.5-flash-high → gemini-3-flash)",
"Fix OAUTH_PROVIDER derivation from BACKEND env var",
"Antigravity E2E test suite: bash ~/.local/bin/test-antigravity.sh",
]),
("3.12.1", "2026-05-27", [
"Fix Antigravity adapter (PR #15): simplified model resolution",
"Removed broken schema sanitization, restored headers",
@@ -483,6 +492,9 @@ def safe_name(name):
digest = hashlib.sha1(name.encode("utf-8")).hexdigest()[:8]
return f"{base}-{digest}"
def _profile_slug(name):
return "".join(ch if ch.isalnum() else "-" for ch in name).strip("-") or "default"
def label_for_backend(backend_type):
return {
"openai-compat": "OpenAI-compatible",
@@ -1032,23 +1044,29 @@ def write_config_for_native(endpoint, selected_model):
mc_path = PROXY_CONFIG_DIR / f"models-{safe_name(endpoint['name'])}.json"
mc_path.parent.mkdir(parents=True, exist_ok=True)
mc_path.write_text(json.dumps(model_catalog, indent=2))
mc_str = str(mc_path).replace("\\", "/")
lines = [
main_lines = [
f'model = "{_toml_safe(selected_model)}"\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
f'model_catalog_json = "{mc_path}"\n',
f'model_catalog_json = "{mc_str}"\n',
f'\n[model_providers."{endpoint["name"]}"]\n',
f'name = "{_toml_safe(endpoint["name"])}"\n',
f'base_url = "{_toml_safe(endpoint["base_url"])}"\n',
f'experimental_bearer_token = "{_toml_safe(_resolve_secret(endpoint["api_key"]))}"\n',
f'\n[profiles."{endpoint["name"]}"]\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
]
write_secure_text(CONFIG, "".join(main_lines))
profile_slug = _profile_slug(endpoint["name"])
profile_path = CONFIG.parent / f"{profile_slug}.config.toml"
profile_lines = [
f'model = "{_toml_safe(selected_model)}"\n',
f'model_catalog_json = "{mc_path}"\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
f'model_catalog_json = "{mc_str}"\n',
f'service_tier = "default"\n',
f'approvals_reviewer = "user"\n',
]
write_secure_text(CONFIG, "".join(lines))
write_secure_text(profile_path, "".join(profile_lines))
def _toml_safe(val):
val = str(val).replace('"', '\\"')
@@ -1067,12 +1085,12 @@ def write_config_for_translated(endpoint, selected_model, proxy_port=8080):
mc_path = PROXY_CONFIG_DIR / f"models-{safe_name(endpoint['name'])}.json"
mc_path.parent.mkdir(parents=True, exist_ok=True)
mc_path.write_text(json.dumps(model_catalog, indent=2))
mc_str = str(mc_path).replace("\\", "/")
lines = [
main_lines = [
f'model = "{_toml_safe(selected_model)}"\n',
f'review_model = "{_toml_safe(selected_model)}"\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
f'model_catalog_json = "{mc_path}"\n',
f'model_catalog_json = "{mc_str}"\n',
f'\n[model_providers."{endpoint["name"]}"]\n',
f'name = "{_toml_safe(endpoint["name"])}"\n',
f'base_url = "http://127.0.0.1:{proxy_port}"\n',
@@ -1081,15 +1099,19 @@ def write_config_for_translated(endpoint, selected_model, proxy_port=8080):
f'request_max_retries = 1\n',
f'stream_max_retries = 0\n',
f'stream_idle_timeout_ms = 600000\n',
f'\n[profiles."{endpoint["name"]}"]\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
]
write_secure_text(CONFIG, "".join(main_lines))
profile_slug = _profile_slug(endpoint["name"])
profile_path = CONFIG.parent / f"{profile_slug}.config.toml"
profile_lines = [
f'model = "{_toml_safe(selected_model)}"\n',
f'review_model = "{_toml_safe(selected_model)}"\n',
f'model_catalog_json = "{mc_path}"\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
f'model_catalog_json = "{mc_str}"\n',
f'service_tier = "fast"\n',
f'approvals_reviewer = "user"\n',
]
write_secure_text(CONFIG, "".join(lines))
write_secure_text(profile_path, "".join(profile_lines))
def _gen_model_catalog(endpoint, selected_model=None):
default_model = selected_model or endpoint.get("default_model")
@@ -1966,6 +1988,9 @@ class LauncherWin(Gtk.Window):
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)
updater_btn = Gtk.Button(label="Update Desktop")
updater_btn.connect("clicked", lambda b: self._open_updater())
hdr.pack_end(updater_btn, False, False, 0)
# verification status bar
self._cli_info = _detect_codex_cli()
@@ -2416,6 +2441,18 @@ class LauncherWin(Gtk.Window):
_py = str(Path(__file__).resolve().parent / "flet-codex-assist.py")
subprocess.Popen([sys.executable, _py], start_new_session=True)
def _open_updater(self):
try:
if not UPDATER_BIN and not _detect_codex_desktop():
self.log("Codex Desktop not installed. Nothing to update.")
return
self._updater_window = CodexUpdaterWindow()
self._updater_window.connect("destroy", lambda *_: setattr(self, "_updater_window", None))
except Exception as e:
import traceback; traceback.print_exc()
d = Gtk.MessageDialog(self, 0, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, f"Error: {e}")
d.run(); d.destroy()
def _backup_profile(self):
chooser = Gtk.FileChooserDialog(
title="Backup Codex Profile",
@@ -2859,7 +2896,7 @@ class LauncherWin(Gtk.Window):
cmd_parts.extend(["codex", "-c", f"model={model}",
"-s", sandbox, "-a", approval])
else:
cmd_parts.extend(["codex", "--profile", ep["name"], "-c", f"model={model}",
cmd_parts.extend(["codex", "--profile", _profile_slug(ep["name"]), "-c", f"model={model}",
"-s", sandbox, "-a", approval])
self.log(f"Running: {' '.join(cmd_parts)}")
@@ -5854,5 +5891,510 @@ class BenchmarkWindow(Gtk.Window):
GLib.idle_add(_show)
# ═══════════════════════════════════════════════════════════════════
# Codex Desktop Updater — auto-update from ilysenko/codex-desktop-linux
# ═══════════════════════════════════════════════════════════════════
UPSTREAM_REPO = "ilysenko/codex-desktop-linux"
UPDATER_BIN = shutil.which("codex-update-manager") or ""
UPDATER_STATE_FILE = Path.home() / ".local/state/codex-update-manager/state.json"
UPDATER_SERVICE_LOG = Path.home() / ".local/state/codex-update-manager/service.log"
def _get_updater_status():
try:
out = subprocess.run(
[UPDATER_BIN, "status", "--json"],
capture_output=True, text=True, timeout=10,
)
if out.returncode == 0 and out.stdout.strip():
return json.loads(out.stdout.strip())
except Exception:
pass
return None
def _get_installed_desktop_version():
try:
out = subprocess.run(
["dpkg-query", "-W", "-f", "${Version}", "codex-desktop"],
capture_output=True, text=True, timeout=5,
)
if out.returncode == 0 and out.stdout.strip():
return out.stdout.strip()
except Exception:
pass
return None
def _get_upstream_info():
try:
req = urllib.request.Request(
f"https://api.github.com/repos/{UPSTREAM_REPO}/commits?per_page=1",
headers={"Accept": "application/vnd.github+json", "User-Agent": "codex-launcher"},
)
resp = urllib.request.urlopen(req, timeout=10)
commits = json.loads(resp.read())
if commits:
c = commits[0]
return {
"sha": c["sha"][:12],
"date": c["commit"]["committer"]["date"][:10],
"message": c["commit"]["message"].split("\n")[0][:80],
}
except Exception:
pass
return None
def _is_updater_service_active():
try:
out = subprocess.run(
["systemctl", "--user", "is-active", "codex-update-manager.service"],
capture_output=True, text=True, timeout=5,
)
return out.stdout.strip() == "active"
except Exception:
return False
class CodexUpdaterWindow(Gtk.Window):
def __init__(self):
super().__init__(title="Codex Desktop Updater")
self.set_default_size(580, 520)
self.set_border_width(10)
self.set_position(Gtk.WindowPosition.CENTER)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=8)
self.add(vbox)
hdr = Gtk.Box(spacing=8)
vbox.pack_start(hdr, False, False, 0)
lbl = Gtk.Label()
lbl.set_markup("<b>Codex Desktop Updater</b>\n<small>Auto-update from github.com/ilysenko/codex-desktop-linux</small>")
lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0)
info_frame = Gtk.Frame(label="Current Installation")
vbox.pack_start(info_frame, False, False, 4)
info_grid = Gtk.Grid(column_spacing=12, row_spacing=4, margin=8)
info_frame.add(info_grid)
self._installed_lbl = Gtk.Label(label="Checking…", xalign=0)
self._service_lbl = Gtk.Label(label="Checking…", xalign=0)
self._upstream_lbl = Gtk.Label(label="Checking…", xalign=0)
self._candidate_lbl = Gtk.Label(label="—", xalign=0)
self._cli_lbl = Gtk.Label(label="Checking…", xalign=0)
labels = [
(0, "Installed:"), (1, self._installed_lbl),
(2, "Upstream:"), (3, self._upstream_lbl),
(4, "Service:"), (5, self._service_lbl),
(6, "Candidate:"), (7, self._candidate_lbl),
(8, "CLI:"), (9, self._cli_lbl),
]
for idx, widget in labels:
if isinstance(widget, str):
widget = Gtk.Label(label=widget, xalign=0)
info_grid.attach(widget, idx % 2, idx // 2, 1, 1)
btn_box = Gtk.Box(spacing=8, homogeneous=True)
vbox.pack_start(btn_box, False, False, 4)
self._check_btn = Gtk.Button(label="Check for Updates")
self._check_btn.connect("clicked", lambda b: self._check_updates())
btn_box.pack_start(self._check_btn, True, True, 0)
self._install_btn = Gtk.Button(label="Install Update")
self._install_btn.connect("clicked", lambda b: self._install_update())
self._install_btn.set_sensitive(False)
self._install_btn.get_style_context().add_class("suggested-action")
btn_box.pack_start(self._install_btn, True, True, 0)
self._rollback_btn = Gtk.Button(label="Rollback")
self._rollback_btn.connect("clicked", lambda b: self._rollback())
self._rollback_btn.set_sensitive(False)
btn_box.pack_start(self._rollback_btn, True, True, 0)
auto_note = Gtk.Label(xalign=0)
auto_note.set_markup("<small>↑ Auto-updater: only detects new upstream <i>Codex.dmg</i> from OpenAI. "
"For latest community patches, use Rebuild from Source below.</small>")
auto_note.set_use_markup(True)
vbox.pack_start(auto_note, False, False, 0)
svc_box = Gtk.Box(spacing=8, homogeneous=True)
vbox.pack_start(svc_box, False, False, 0)
self._svc_start_btn = Gtk.Button(label="Start Service")
self._svc_start_btn.connect("clicked", lambda b: self._toggle_service("start"))
svc_box.pack_start(self._svc_start_btn, True, True, 0)
self._svc_stop_btn = Gtk.Button(label="Stop Service")
self._svc_stop_btn.connect("clicked", lambda b: self._toggle_service("stop"))
svc_box.pack_start(self._svc_stop_btn, True, True, 0)
self._svc_enable_btn = Gtk.Button(label="Enable Autostart")
self._svc_enable_btn.connect("clicked", lambda b: self._toggle_service("enable"))
svc_box.pack_start(self._svc_enable_btn, True, True, 0)
rebuild_box = Gtk.Box(spacing=8)
vbox.pack_start(rebuild_box, False, False, 4)
rebuild_info = Gtk.Label(xalign=0)
rebuild_info.set_markup(
"<b>Rebuild from Source (Recommended)</b>\n"
"<small>The auto-updater only detects new upstream Codex DMGs from OpenAI's CDN.\n"
"To get the <i>latest community fixes</i> from ilysenko/codex-desktop-linux,\n"
"use Clone/Pull then Build &amp; Install to rebuild a fresh .deb from source.</small>"
)
rebuild_info.set_use_markup(True)
rebuild_box.pack_start(rebuild_info, True, True, 0)
rebuild_btn_box = Gtk.Box(spacing=8)
vbox.pack_start(rebuild_btn_box, False, False, 0)
self._clone_btn = Gtk.Button(label="Clone / Pull Repo")
self._clone_btn.connect("clicked", lambda b: self._clone_or_pull())
rebuild_btn_box.pack_start(self._clone_btn, True, True, 0)
self._build_btn = Gtk.Button(label="Build & Install .deb")
self._build_btn.connect("clicked", lambda b: self._build_and_install())
self._build_btn.set_sensitive(False)
self._build_btn.get_style_context().add_class("suggested-action")
rebuild_btn_box.pack_start(self._build_btn, True, True, 0)
self._rebuild_dir_lbl = Gtk.Label(label="", xalign=0)
vbox.pack_start(self._rebuild_dir_lbl, False, False, 0)
sw = Gtk.ScrolledWindow()
sw.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
vbox.pack_start(sw, True, True, 0)
self._log_buf = Gtk.TextBuffer()
tv = Gtk.TextView(buffer=self._log_buf)
tv.set_editable(False)
tv.set_cursor_visible(False)
tv.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
sw.add(tv)
bb = Gtk.Box(spacing=8)
vbox.pack_start(bb, False, False, 0)
clear_btn = Gtk.Button(label="Clear Log")
clear_btn.connect("clicked", lambda b: self._log_buf.set_text(""))
bb.pack_start(clear_btn, False, False, 0)
view_log_btn = Gtk.Button(label="View Service Log")
view_log_btn.connect("clicked", lambda b: self._view_service_log())
bb.pack_start(view_log_btn, False, False, 0)
close_btn = Gtk.Button(label="Close")
close_btn.connect("clicked", lambda b: self.destroy())
bb.pack_end(close_btn, False, False, 0)
self.show_all()
self._rebuild_dir = Path.home() / ".cache/codex-launcher/codex-desktop-linux"
self._rebuild_dir_lbl.set_markup(f"<small>Build dir: {self._rebuild_dir}</small>")
self._rebuild_dir_lbl.set_use_markup(True)
self._log("Updater initialized")
threading.Thread(target=self._refresh_status, daemon=True).start()
def _log(self, msg):
def _append():
e = self._log_buf.get_end_iter()
self._log_buf.insert(e, msg + "\n")
GLib.idle_add(_append)
def _refresh_status(self):
installed = _get_installed_desktop_version()
upstream = _get_upstream_info()
status = _get_updater_status()
svc_active = _is_updater_service_active()
def _update():
if installed:
self._installed_lbl.set_markup(f"<span foreground='#2ea043'><b>{installed}</b></span>")
self._installed_lbl.set_use_markup(True)
else:
self._installed_lbl.set_text("Not installed via dpkg")
if upstream:
self._upstream_lbl.set_markup(
f"<span foreground='#2ea043'>{upstream['date']}</span>"
f" <small>({upstream['sha']}) {upstream['message']}</small>"
)
self._upstream_lbl.set_use_markup(True)
else:
self._upstream_lbl.set_text("Could not fetch")
if svc_active:
self._service_lbl.set_markup("<span foreground='#2ea043'>● active</span>")
self._service_lbl.set_use_markup(True)
else:
self._service_lbl.set_markup("<span foreground='#d29922'>● inactive</span>")
self._service_lbl.set_use_markup(True)
if status:
cand = status.get("candidate_version")
if cand:
self._candidate_lbl.set_markup(f"<span foreground='#58a6ff'><b>{cand}</b></span>")
self._candidate_lbl.set_use_markup(True)
self._install_btn.set_sensitive(True)
else:
self._candidate_lbl.set_text("No update pending")
self._install_btn.set_sensitive(False)
cli_ver = status.get("cli_installed_version", "")
cli_latest = status.get("cli_latest_version", "")
cli_status = status.get("cli_status", "")
if cli_ver:
color = "#2ea043" if cli_status == "up_to_date" else "#d29922"
self._cli_lbl.set_markup(
f"<span foreground='{color}'>{cli_ver}"
f"{' (up to date)' if cli_status == 'up_to_date' else f' → {cli_latest}'}"
f"</span>"
)
self._cli_lbl.set_use_markup(True)
has_rollback = bool(status.get("last_known_good_version"))
self._rollback_btn.set_sensitive(has_rollback)
else:
if not UPDATER_BIN:
self._candidate_lbl.set_text("codex-update-manager not found")
else:
self._candidate_lbl.set_text("Status unavailable")
if self._rebuild_dir.exists():
self._build_btn.set_sensitive(True)
GLib.idle_add(_update)
self._log(f"Status: installed={installed} svc={'active' if svc_active else 'inactive'}")
def _check_updates(self):
self._check_btn.set_sensitive(False)
self._log("Checking for updates…")
def _run():
try:
out = subprocess.run(
[UPDATER_BIN, "check-now"],
capture_output=True, text=True, timeout=120,
)
self._log(f"check-now: rc={out.returncode}")
if out.stdout:
self._log(out.stdout.strip())
if out.stderr:
self._log(out.stderr.strip())
except Exception as e:
self._log(f"Error: {e}")
finally:
GLib.idle_add(lambda: self._check_btn.set_sensitive(True))
self._refresh_status()
threading.Thread(target=_run, daemon=True).start()
def _install_update(self):
self._install_btn.set_sensitive(False)
self._log("Installing update (may prompt for sudo)…")
def _run():
try:
desktop_running = False
try:
out = subprocess.run(
["pgrep", "-f", "/opt/codex-desktop/electron"],
capture_output=True, text=True, timeout=5,
)
desktop_running = out.returncode == 0
except Exception:
pass
if desktop_running:
self._log("⚠ Codex Desktop is running. Closing it to proceed with update…")
subprocess.run(["pkill", "-f", "/opt/codex-desktop/electron"], timeout=10)
import time; time.sleep(3)
self._log("Desktop closed.")
out = subprocess.run(
[UPDATER_BIN, "install-ready"],
capture_output=True, text=True, timeout=300,
)
self._log(f"install-ready: rc={out.returncode}")
combined = (out.stdout or "") + (out.stderr or "")
if out.stdout:
self._log(out.stdout.strip())
if out.stderr:
self._log(out.stderr.strip())
if out.returncode == 0 and "successfully" in combined.lower():
self._log("Update installed successfully!")
elif "No Codex Desktop update is ready" in combined:
self._log("⚠ No update is ready. Run 'Check for Updates' first, or use 'Clone/Pull + Build & Install' for a manual update.")
GLib.idle_add(lambda: self._install_btn.set_sensitive(False))
elif "Close it to install" in combined:
self._log("⚠ Desktop was still running. Close Desktop manually and try again.")
GLib.idle_add(lambda: self._install_btn.set_sensitive(True))
elif out.returncode == 0:
self._log("⚠ install-ready returned OK but no confirmation of actual install. Output: " + combined[:200])
GLib.idle_add(lambda: self._install_btn.set_sensitive(False))
else:
self._log("Update may not have completed. Check the log above.")
GLib.idle_add(lambda: self._install_btn.set_sensitive(True))
except Exception as e:
self._log(f"Error: {e}")
finally:
self._refresh_status()
threading.Thread(target=_run, daemon=True).start()
def _rollback(self):
self._rollback_btn.set_sensitive(False)
self._log("Rolling back to previous version…")
def _run():
try:
out = subprocess.run(
[UPDATER_BIN, "rollback"],
capture_output=True, text=True, timeout=300,
)
self._log(f"rollback: rc={out.returncode}")
if out.stdout:
self._log(out.stdout.strip())
if out.stderr:
self._log(out.stderr.strip())
except Exception as e:
self._log(f"Error: {e}")
finally:
self._refresh_status()
threading.Thread(target=_run, daemon=True).start()
def _toggle_service(self, action):
cmd_map = {
"start": ["systemctl", "--user", "start", "codex-update-manager.service"],
"stop": ["systemctl", "--user", "stop", "codex-update-manager.service"],
"enable": ["systemctl", "--user", "enable", "--now", "codex-update-manager.service"],
}
cmd = cmd_map.get(action)
if not cmd:
return
self._log(f"Running: {' '.join(cmd)}")
def _run():
try:
out = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
self._log(f"{action}: rc={out.returncode}")
if out.stderr:
self._log(out.stderr.strip())
except Exception as e:
self._log(f"Error: {e}")
finally:
self._refresh_status()
threading.Thread(target=_run, daemon=True).start()
def _clone_or_pull(self):
self._clone_btn.set_sensitive(False)
self._log(f"Clone/pull {UPSTREAM_REPO}…")
def _run():
try:
self._rebuild_dir.parent.mkdir(parents=True, exist_ok=True)
if self._rebuild_dir.exists():
self._log("Pulling latest changes…")
out = subprocess.run(
["git", "pull", "--ff-only"],
capture_output=True, text=True, timeout=60,
cwd=str(self._rebuild_dir),
)
else:
self._log("Cloning repository…")
out = subprocess.run(
["git", "clone", "--depth=1", f"https://github.com/{UPSTREAM_REPO}.git", str(self._rebuild_dir)],
capture_output=True, text=True, timeout=120,
)
self._log(f"git: rc={out.returncode}")
if out.stdout:
self._log(out.stdout.strip()[:200])
if out.stderr:
self._log(out.stderr.strip()[:200])
if out.returncode == 0:
self._log("Repository ready.")
GLib.idle_add(lambda: self._build_btn.set_sensitive(True))
except Exception as e:
self._log(f"Error: {e}")
finally:
GLib.idle_add(lambda: self._clone_btn.set_sensitive(True))
threading.Thread(target=_run, daemon=True).start()
def _build_and_install(self):
self._build_btn.set_sensitive(False)
self._log("Building Codex Desktop from source (this may take several minutes)…")
def _run():
try:
self._log("Installing build dependencies…")
out = subprocess.run(
["bash", "-c", "bash scripts/install-deps.sh"],
capture_output=True, text=True, timeout=300,
cwd=str(self._rebuild_dir),
)
self._log(f"install-deps: rc={out.returncode}")
if out.stderr:
self._log(out.stderr.strip()[-300:])
self._log("Building app from upstream DMG…")
out = subprocess.run(
["make", "build-app-fresh"],
capture_output=True, text=True, timeout=600,
cwd=str(self._rebuild_dir),
)
self._log(f"build-app-fresh: rc={out.returncode}")
if out.stderr:
self._log(out.stderr.strip()[-300:])
if out.returncode != 0:
self._log("Build failed. Check log above.")
return
self._log("Building .deb package…")
out = subprocess.run(
["make", "deb"],
capture_output=True, text=True, timeout=120,
cwd=str(self._rebuild_dir),
)
self._log(f"deb: rc={out.returncode}")
if out.stderr:
self._log(out.stderr.strip()[-300:])
if out.returncode != 0:
self._log("Deb build failed.")
return
deb_files = list((self._rebuild_dir / "dist").glob("codex-desktop_*.deb"))
if not deb_files:
self._log("No .deb found in dist/")
return
deb_path = deb_files[-1]
self._log(f"Installing {deb_path.name}…")
out = subprocess.run(
["pkexec", "dpkg", "-i", str(deb_path)],
capture_output=True, text=True, timeout=120,
)
self._log(f"dpkg -i: rc={out.returncode}")
if out.stdout:
self._log(out.stdout.strip()[:300])
if out.stderr:
self._log(out.stderr.strip()[:300])
if out.returncode == 0:
self._log("Codex Desktop updated successfully!")
else:
self._log("Installation failed. Try: sudo dpkg -i " + str(deb_path))
except Exception as e:
self._log(f"Error: {e}")
finally:
self._refresh_status()
threading.Thread(target=_run, daemon=True).start()
def _view_service_log(self):
if UPDATER_SERVICE_LOG.exists():
subprocess.Popen(["xdg-open", str(UPDATER_SERVICE_LOG)])
else:
self._log(f"Service log not found at {UPDATER_SERVICE_LOG}")
if __name__ == "__main__":
main()

View File

@@ -33,6 +33,7 @@ from codex_launcher_lib import (
PROVIDER_PRESETS, CHANGELOG, DEFAULT_CONFIG, OAUTH_SECRETS_PATH,
ANTIGRAVITY_MODELS,
safe_name, label_for_backend, normalize_model_id, normalize_base_url,
_profile_slug,
parse_model_list, now_utc_iso, apply_provider_preset,
load_endpoints, save_endpoints, load_bgp_pools, save_bgp_pools,
get_endpoint, build_profile_bundle, save_profile_bundle, import_profile_bundle,
@@ -2073,6 +2074,164 @@ class BenchmarkWindow:
self._dlg.after(0, _show)
# ═══════════════════════════════════════════════════════════════════════
# Codex Desktop Updater Window
# ═══════════════════════════════════════════════════════════════════════
class UpdateDesktopWindow:
def __init__(self, parent):
self._dlg = tk.Toplevel(parent)
self._dlg.title("Codex Desktop Updater")
self._dlg.geometry("580x520")
self._dlg.transient(parent)
main = ttk.Frame(self._dlg, padding=12)
main.pack(fill="both", expand=True)
ttk.Label(main, text="Codex Desktop Updater",
font=("Segoe UI", 11, "bold")).pack(anchor="w")
if IS_WINDOWS:
info_frame = ttk.LabelFrame(main, text="Status", padding=8)
info_frame.pack(fill="x", pady=(8, 0))
ttk.Label(info_frame,
text="Update feature available on Linux only.\n"
"On Windows, use the official Codex Desktop installer\n"
"or download updates from https://codex.desktop.openai.com",
foreground="#d29922").pack(anchor="w")
ttk.Button(self._dlg, text="Close",
command=self._dlg.destroy).pack(pady=(8, 0))
return
self._status_text = scrolledtext.ScrolledText(main, height=14, state="disabled",
wrap="word", font=("Consolas", 9))
self._status_text.pack(fill="both", expand=True, pady=(8, 0))
btn_frame = ttk.Frame(main)
btn_frame.pack(fill="x", pady=(8, 0))
ttk.Button(btn_frame, text="Refresh Status",
command=self._refresh_status).pack(side="left", padx=(0, 4))
ttk.Button(btn_frame, text="Check for Updates",
command=self._check_updates).pack(side="left", padx=(0, 4))
ttk.Button(btn_frame, text="Install Update",
command=self._install_update).pack(side="left", padx=(0, 4))
ttk.Button(btn_frame, text="Rollback",
command=self._rollback).pack(side="left", padx=(0, 4))
svc_frame = ttk.Frame(main)
svc_frame.pack(fill="x", pady=(4, 0))
ttk.Button(svc_frame, text="Start Service",
command=lambda: self._svc_cmd("start")).pack(side="left", padx=(0, 4))
ttk.Button(svc_frame, text="Stop Service",
command=lambda: self._svc_cmd("stop")).pack(side="left", padx=(0, 4))
ttk.Button(svc_frame, text="Enable Service",
command=lambda: self._svc_cmd("enable")).pack(side="left", padx=(0, 4))
manual_frame = ttk.LabelFrame(main, text="Manual Rebuild (ilysenko/codex-desktop-linux)", padding=4)
manual_frame.pack(fill="x", pady=(8, 0))
ttk.Button(manual_frame, text="Clone/Pull Repo",
command=self._clone_pull).pack(side="left", padx=(0, 4))
ttk.Button(manual_frame, text="Build & Install .deb",
command=self._build_install).pack(side="left", padx=(0, 4))
ttk.Button(main, text="Close",
command=self._dlg.destroy).pack(side="right", pady=(8, 0))
self._refresh_status()
def _set_status(self, text):
self._status_text.configure(state="normal")
self._status_text.delete("1.0", "end")
self._status_text.insert("end", text)
self._status_text.configure(state="disabled")
def _run_cmd(self, cmd, label):
def _thread():
try:
r = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
out = r.stdout.strip() or r.stderr.strip() or "(no output)"
self._dlg.after(0, lambda: self._set_status(f"[{label}]\n{out}"))
except FileNotFoundError:
self._dlg.after(0, lambda: self._set_status(
f"[{label}] codex-update-manager not found.\n"
"Install from ilysenko/codex-desktop-linux"))
except Exception as e:
self._dlg.after(0, lambda: self._set_status(f"[{label}] Error: {e}"))
threading.Thread(target=_thread, daemon=True).start()
def _refresh_status(self):
if IS_WINDOWS:
return
def _thread():
try:
r = subprocess.run(["codex-update-manager", "status", "--json"],
capture_output=True, text=True, timeout=30)
if r.returncode == 0 and r.stdout.strip():
data = json.loads(r.stdout.strip())
lines = []
lines.append(f"Installed version: {data.get('installed_version', 'unknown')}")
lines.append(f"Upstream version: {data.get('upstream_version', 'unknown')}")
lines.append(f"Upstream date: {data.get('upstream_date', 'unknown')}")
lines.append(f"Update available: {data.get('update_available', False)}")
lines.append(f"Service status: {data.get('service_status', 'unknown')}")
lines.append(f"Service enabled: {data.get('service_enabled', False)}")
if data.get("last_check"):
lines.append(f"Last check: {data['last_check']}")
if data.get("error"):
lines.append(f"Error: {data['error']}")
self._dlg.after(0, lambda: self._set_status("\n".join(lines)))
else:
err = r.stderr.strip() or r.stdout.strip() or "unknown error"
self._dlg.after(0, lambda: self._set_status(f"Status error:\n{err}"))
except FileNotFoundError:
self._dlg.after(0, lambda: self._set_status(
"codex-update-manager not found.\n"
"Install from ilysenko/codex-desktop-linux"))
except Exception as e:
self._dlg.after(0, lambda: self._set_status(f"Status error: {e}"))
threading.Thread(target=_thread, daemon=True).start()
def _check_updates(self):
if IS_WINDOWS:
return
self._run_cmd(["codex-update-manager", "check-now"], "Check for Updates")
def _install_update(self):
if IS_WINDOWS:
return
self._run_cmd(["codex-update-manager", "install-ready"], "Install Update")
def _rollback(self):
if IS_WINDOWS:
return
self._run_cmd(["codex-update-manager", "rollback"], "Rollback")
def _svc_cmd(self, action):
if IS_WINDOWS:
return
self._run_cmd(["systemctl", "--user", action, "codex-update-manager"],
f"Service {action}")
def _clone_pull(self):
if IS_WINDOWS:
return
repo_dir = str(Path.home() / "codex-desktop-linux")
if Path(repo_dir).exists():
self._run_cmd(["git", "-C", repo_dir, "pull"], "Pull Repo")
else:
self._run_cmd(["git", "clone",
"https://github.com/ilysenko/codex-desktop-linux",
repo_dir], "Clone Repo")
def _build_install(self):
if IS_WINDOWS:
return
repo_dir = str(Path.home() / "codex-desktop-linux")
self._run_cmd(["bash", "-c",
f"cd {repo_dir} && bash build-install.sh"],
"Build & Install .deb")
# ═══════════════════════════════════════════════════════════════════════
# Main Launcher Window
# ═══════════════════════════════════════════════════════════════════════
@@ -2161,6 +2320,7 @@ class LauncherWin:
ttk.Button(tb1, text="History", command=self._open_history).pack(side="left", padx=(6, 0))
ttk.Button(tb1, text="OAuth Secrets", command=self._edit_oauth_secrets).pack(side="left", padx=(6, 0))
ttk.Button(tb1, text="Changelog", command=self._show_changelog).pack(side="right")
ttk.Button(tb1, text="Update Desktop", command=self._open_updater).pack(side="right", padx=(0, 6))
# Detection status — one row per item so long paths don't truncate
self._cli_info = detect_codex_cli()
@@ -2407,6 +2567,9 @@ class LauncherWin:
def _open_benchmark(self):
BenchmarkWindow(self._root)
def _open_updater(self):
UpdateDesktopWindow(self._root)
def _open_proxy_log_dir(self):
log_dir = str(PROXY_CONFIG_DIR)
req_log = PROXY_CONFIG_DIR / "requests.log"
@@ -3184,7 +3347,7 @@ class LauncherWin:
if ep["backend_type"] == "native":
cmd_parts.extend(["codex", "-c", f"model={model}"])
else:
cmd_parts.extend(["codex", "--profile", ep["name"], "-c", f"model={model}"])
cmd_parts.extend(["codex", "--profile", _profile_slug(ep["name"]), "-c", f"model={model}"])
self.log(f"Running: {' '.join(cmd_parts)}")
if IS_WINDOWS:

View File

@@ -83,6 +83,13 @@ model_catalog_json = ""
"""
CHANGELOG = [
("3.13.0", "2026-05-27", [
"Codex Desktop Updater: auto-update from ilysenko/codex-desktop-linux",
"Fix Antigravity: prod endpoint first, model resolution, OAUTH_PROVIDER derivation",
"Fix Codex CLI 0.134.0 profile system: separate .config.toml files",
"Fix compaction: max_input_items 60->200 for 1M-token Antigravity models",
"Antigravity E2E test suite: bash test-antigravity.sh",
]),
("3.12.1", "2026-05-27", [
"Fix Antigravity adapter (PR #15): simplify model resolution",
"Removed broken schema sanitization, restored correct headers",
@@ -714,6 +721,9 @@ def safe_name(name):
digest = hashlib.sha1(name.encode("utf-8")).hexdigest()[:8]
return f"{base}-{digest}"
def _profile_slug(name):
return "".join(ch if ch.isalnum() else "-" for ch in name).strip("-") or "default"
def label_for_backend(backend_type):
return {
@@ -1104,10 +1114,9 @@ def write_config_for_native(endpoint, selected_model):
mc_path = PROXY_CONFIG_DIR / f"models-{safe_name(endpoint['name'])}.json"
mc_path.parent.mkdir(parents=True, exist_ok=True)
mc_path.write_text(json.dumps(model_catalog, indent=2))
mc_str = str(mc_path).replace("\\", "/")
new_config = [
f'profile = "{_toml_safe(endpoint["name"])}"\n',
main_config = [
f'model = "{_toml_safe(selected_model)}"\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
f'model_catalog_json = "{mc_str}"\n',
@@ -1115,16 +1124,21 @@ def write_config_for_native(endpoint, selected_model):
f'name = "{_toml_safe(endpoint["name"])}"\n',
f'base_url = "{_toml_safe(endpoint["base_url"])}"\n',
f'experimental_bearer_token = "{_toml_safe(_resolve_secret(endpoint["api_key"]))}"\n',
f'\n[profiles."{endpoint["name"]}"]\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
]
existing = CONFIG.read_text(encoding="utf-8") if CONFIG.exists() else ""
merged = _merge_toml(existing, "".join(main_config))
write_secure_text(CONFIG, merged)
profile_slug = _profile_slug(endpoint["name"])
profile_path = CONFIG.parent / f"{profile_slug}.config.toml"
profile_lines = [
f'model = "{_toml_safe(selected_model)}"\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
f'model_catalog_json = "{mc_str}"\n',
f'service_tier = "default"\n',
f'approvals_reviewer = "user"\n',
]
existing = CONFIG.read_text(encoding="utf-8") if CONFIG.exists() else ""
merged = _merge_toml(existing, "".join(new_config))
write_secure_text(CONFIG, merged)
write_secure_text(profile_path, "".join(profile_lines))
def write_config_for_translated(endpoint, selected_model, proxy_port=8080):
@@ -1133,10 +1147,9 @@ def write_config_for_translated(endpoint, selected_model, proxy_port=8080):
mc_path = PROXY_CONFIG_DIR / f"models-{safe_name(endpoint['name'])}.json"
mc_path.parent.mkdir(parents=True, exist_ok=True)
mc_path.write_text(json.dumps(model_catalog, indent=2))
mc_str = str(mc_path).replace("\\", "/")
new_config = [
f'profile = "{_toml_safe(endpoint["name"])}"\n',
main_config = [
f'model = "{_toml_safe(selected_model)}"\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
f'model_catalog_json = "{mc_str}"\n',
@@ -1144,16 +1157,21 @@ def write_config_for_translated(endpoint, selected_model, proxy_port=8080):
f'name = "{_toml_safe(endpoint["name"])}"\n',
f'base_url = "http://127.0.0.1:{proxy_port}"\n',
f'experimental_bearer_token = "codex-launcher-local"\n',
f'\n[profiles."{endpoint["name"]}"]\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
]
existing = CONFIG.read_text(encoding="utf-8") if CONFIG.exists() else ""
merged = _merge_toml(existing, "".join(main_config))
write_secure_text(CONFIG, merged)
profile_slug = _profile_slug(endpoint["name"])
profile_path = CONFIG.parent / f"{profile_slug}.config.toml"
profile_lines = [
f'model = "{_toml_safe(selected_model)}"\n',
f'model_provider = "{_toml_safe(endpoint["name"])}"\n',
f'model_catalog_json = "{mc_str}"\n',
f'service_tier = "fast"\n',
f'approvals_reviewer = "user"\n',
]
existing = CONFIG.read_text(encoding="utf-8") if CONFIG.exists() else ""
merged = _merge_toml(existing, "".join(new_config))
write_secure_text(CONFIG, merged)
write_secure_text(profile_path, "".join(profile_lines))
# ═══════════════════════════════════════════════════════════════════════
# Model fetching

View File

@@ -1031,6 +1031,10 @@ def _init_runtime():
TARGET_URL = CONFIG["target_url"].rstrip("/")
API_KEY = CONFIG["api_key"]
OAUTH_PROVIDER = CONFIG.get("oauth_provider") or ""
if not OAUTH_PROVIDER and BACKEND == "gemini-oauth-antigravity":
OAUTH_PROVIDER = "google-antigravity"
if not OAUTH_PROVIDER and BACKEND == "gemini-oauth":
OAUTH_PROVIDER = "google-cli"
MODELS = CONFIG["models"]
CC_VERSION = CONFIG.get("cc_version", "")
REASONING_ENABLED = CONFIG.get("reasoning_enabled", True)
@@ -2007,10 +2011,10 @@ _PROVIDER_POLICIES = {
"openadapter": {"reasoning_mode": "off", "max_tokens": 32768, "strip_reasoning": True,
"tool_output_limit": 1000, "max_input_items": 10, "compaction": "aggressive",
"synthetic_tool_results": True},
"cloudcode-pa": {"compaction": "aggressive", "context_size": 1000000,
"tool_output_limit": 6000, "max_input_items": 60},
"googleapis": {"compaction": "balanced", "context_size": 1000000,
"tool_output_limit": 6000, "max_input_items": 80},
"cloudcode-pa": {"compaction": "conservative", "context_size": 1000000,
"tool_output_limit": 8000, "max_input_items": 200},
"googleapis": {"compaction": "conservative", "context_size": 1000000,
"tool_output_limit": 8000, "max_input_items": 250},
}
def provider_policy(target_url=None, backend=None):
@@ -5544,6 +5548,28 @@ class Handler(http.server.BaseHTTPRequestHandler):
return chat_body
def _handle_antigravity_v2(self, body, model, stream, tracker=None):
_model_alias = {
"gemini-3.5-flash-high": "gemini-3-flash",
"gemini-3.5-flash-medium": "gemini-3-flash",
"gemini-3.5-flash-low": "gemini-3.5-flash-low",
"gemini-3.5-flash": "gemini-3-flash",
"gemini-3-flash-preview": "gemini-3-flash",
"gemini-3-pro-preview": "gemini-3.1-pro-low",
"gemini-3-pro": "gemini-3.1-pro-low",
"gemini-3-pro-low": "gemini-3.1-pro-low",
"gemini-3-pro-high": "gemini-3.1-pro-low",
"gemini-3.1-pro": "gemini-3.1-pro-low",
"gemini-3.1-pro-high": "gemini-3.1-pro-low",
"claude-sonnet-4.6": "claude-sonnet-4-6",
"claude-sonnet-4.6-thinking": "claude-sonnet-4-6",
"claude-opus-4.6": "claude-opus-4-6-thinking",
"claude-opus-4.6-thinking": "claude-opus-4-6-thinking",
}
_resolved = _model_alias.get(model, model)
if _resolved != model:
print(f"[{getattr(self, '_session_id', '?')}] [antigravity-v2] model resolved: {model} -> {_resolved}", file=sys.stderr)
model = _resolved
input_data = body.get("input", "")
_schema = _load_schema(model=model)
if _schema and not _schema.supports_vision:
@@ -5810,11 +5836,10 @@ class Handler(http.server.BaseHTTPRequestHandler):
}
wrapped["request"]["sessionId"] = f"{uuid.uuid4().hex}{int(time.time()*1000)}"
# Use endpoint order from repo4/opencode-antigravity-auth: daily sandbox → autopush sandbox → prod
_antigravity_endpoints = [
"https://cloudcode-pa.googleapis.com",
"https://daily-cloudcode-pa.sandbox.googleapis.com",
"https://autopush-cloudcode-pa.sandbox.googleapis.com",
"https://cloudcode-pa.googleapis.com",
]
body_b = json.dumps(wrapped).encode()
@@ -5861,8 +5886,12 @@ class Handler(http.server.BaseHTTPRequestHandler):
pass
if e.code == 400:
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}})
if err_class in ("auth_permanent", "service_disabled", "forbidden", "account_banned", "validation_required"):
if err_class in ("auth_permanent", "forbidden", "account_banned", "validation_required"):
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}})
if err_class == "service_disabled":
_is_prod = "cloudcode-pa.googleapis.com" in ep and "sandbox" not in ep
if _is_prod:
return self.send_json(e.code, {"error": {"type": "upstream_error", "message": _sanitize_err_body(err_body)}})
if err_class in ("quota_exhausted", "rate_limited"):
pool = _google_antigravity_pool
_, acct = _get_google_account(OAUTH_PROVIDER)