diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3511a55..ed7a3df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,22 @@
# Changelog
+## v3.10.0 (2026-05-25)
+
+**Provider Model Editor + Antigravity Model Refresh**
+
+### Provider Editor
+- **Remove Selected** button to remove highlighted model(s) from provider
+- **Clear All** button to empty model list
+- **Sync from Preset** button to refresh model list from current preset definition
+- Preset sync now replaces (not appends) models — fixes stale saved model lists
+
+### Antigravity Models Updated
+- **Gemini 3.5 Flash** (High / Medium)
+- **Gemini 3.1 Pro** (High / Low)
+- **Claude Sonnet 4.6 Thinking**
+- **Claude Opus 4.6 Thinking**
+- **GPT-OSS 120B Medium**
+
## v3.9.9 (2026-05-25)
**Antigravity Model Refresh**
diff --git a/codex-launcher_3.10.0_all.deb b/codex-launcher_3.10.0_all.deb
new file mode 100644
index 0000000..668df2a
Binary files /dev/null and b/codex-launcher_3.10.0_all.deb differ
diff --git a/install.sh b/install.sh
index 79ad396..f1e7266 100755
--- a/install.sh
+++ b/install.sh
@@ -3,11 +3,11 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-if [ -f "$SCRIPT_DIR/codex-launcher_3.9.9_all.deb" ]; then
- echo "Installing codex-launcher_3.9.9_all.deb ..."
- sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.9.9_all.deb"
+if [ -f "$SCRIPT_DIR/codex-launcher_3.10.0_all.deb" ]; then
+ echo "Installing codex-launcher_3.10.0_all.deb ..."
+ sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.0_all.deb"
echo ""
- echo "Installed v3.9.9 via .deb package."
+ echo "Installed v3.10.0 via .deb package."
echo " translate-proxy.py -> /usr/bin/translate-proxy.py"
echo " codex-launcher-gui -> /usr/bin/codex-launcher-gui"
echo " cleanup-codex-stale -> /usr/bin/cleanup-codex-stale.sh"
diff --git a/src/codex-launcher-gui b/src/codex-launcher-gui
index bd65e74..9ca1e96 100755
--- a/src/codex-launcher-gui
+++ b/src/codex-launcher-gui
@@ -26,6 +26,11 @@ model_catalog_json = ""
"""
CHANGELOG = [
+ ("3.10.0", "2026-05-25", [
+ "Provider editor: Remove Selected, Clear All, Sync from Preset buttons for model list",
+ "Sync from Preset replaces model list with current preset models",
+ "Stale saved Antigravity models auto-refreshed on preset sync",
+ ]),
("3.9.9", "2026-05-25", [
"Refresh Antigravity preset: Gemini 3.5 Flash, Gemini 3.1 Pro, Claude Sonnet/Opus 4.6, GPT-OSS 120B",
"Fix Antigravity alias map for new tiered model IDs (high/medium/low/thinking)",
@@ -1752,7 +1757,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.9.9")
+ lbl = Gtk.Label(label="Codex Launcher v3.10.0")
lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0)
changelog_btn = Gtk.Button(label="Changelog")
@@ -3138,6 +3143,18 @@ class EditEndpointDialog(Gtk.Dialog):
sw.add(self._model_tree)
self._model_tree.connect("row-activated", lambda t, p, c: self._remove_model(p))
+ model_btn_box = Gtk.Box(spacing=6)
+ area.pack_start(model_btn_box, False, False, 0)
+ self._remove_model_btn = Gtk.Button(label="Remove Selected")
+ self._remove_model_btn.connect("clicked", lambda b: self._remove_selected_model())
+ model_btn_box.pack_start(self._remove_model_btn, False, False, 0)
+ self._clear_models_btn = Gtk.Button(label="Clear All")
+ self._clear_models_btn.connect("clicked", lambda b: self._clear_all_models())
+ model_btn_box.pack_start(self._clear_models_btn, False, False, 0)
+ self._sync_preset_btn = Gtk.Button(label="Sync from Preset")
+ self._sync_preset_btn.connect("clicked", lambda b: self._apply_selected_preset())
+ model_btn_box.pack_start(self._sync_preset_btn, False, False, 0)
+
for m in self._data.get("models", []):
self._model_store.append([m])
@@ -3207,10 +3224,12 @@ class EditEndpointDialog(Gtk.Dialog):
cc_ver = preset.get("cc_version", "")
if cc_ver and not self._entry_cc_ver.get_text().strip():
self._entry_cc_ver.set_text(cc_ver)
- if preset.get("models") and len(self._model_store) == 0:
- for mid in preset["models"]:
- self._model_store.append([mid])
- self._refresh_default_combo(preset["models"][0])
+ if preset.get("models") and (not initial or len(self._model_store) == 0):
+ current = self._combo_default.get_active_text()
+ self._model_store.clear()
+ for mid in preset["models"]:
+ self._model_store.append([mid])
+ self._refresh_default_combo(current or preset["models"][0])
if initial and self._data.get("models"):
self._refresh_default_combo(self._data.get("default_model", ""))
@@ -3542,7 +3561,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.9.9"})
+ headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.0"})
resp = urllib.request.urlopen(req, timeout=30)
data = json.loads(resp.read())
login_url = data.get("loginUrl", "") or data.get("login_url", "")
@@ -3567,7 +3586,7 @@ class EditEndpointDialog(Gtk.Dialog):
time.sleep(2)
try:
poll_req = urllib.request.Request(poll_url,
- headers={"User-Agent": "codex-launcher/3.9.9"})
+ headers={"User-Agent": "codex-launcher/3.10.0"})
poll_resp = urllib.request.urlopen(poll_req, timeout=10)
poll_data = json.loads(poll_resp.read())
user = poll_data.get("user")
@@ -3632,6 +3651,21 @@ class EditEndpointDialog(Gtk.Dialog):
self._model_store.remove(self._model_store.get_iter(path))
self._refresh_default_combo(current)
+ def _remove_selected_model(self):
+ sel = self._model_tree.get_selection()
+ model, paths = sel.get_selected_rows()
+ if not paths:
+ return
+ current = self._combo_default.get_active_text()
+ for p in reversed(paths):
+ self._model_store.remove(self._model_store.get_iter(p))
+ self._refresh_default_combo(current)
+
+ def _clear_all_models(self):
+ current = self._combo_default.get_active_text()
+ self._model_store.clear()
+ self._refresh_default_combo(current)
+
def _refresh_default_combo(self, active=None):
if active is None:
active = self._combo_default.get_active_text()
diff --git a/src/translate-proxy.py b/src/translate-proxy.py
index ede42a8..0015378 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.9.9",
+ "User-Agent": "codex-launcher/3.10.0",
"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.9.9",
+ "User-Agent": "codex-launcher/3.10.0",
})
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.9.9",
+ "User-Agent": "codex-launcher/3.10.0",
})
try:
urllib.request.urlopen(req, timeout=10)
@@ -5314,7 +5314,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}",
- "User-Agent": "codex-launcher/3.9.9",
+ "User-Agent": "codex-launcher/3.10.0",
"x-codebuff-model": model,
}
if instance_id:
@@ -5480,7 +5480,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.9.9", "x-codebuff-model": model}
+ headers = {"Content-Type": "application/json", "Authorization": f"Bearer {token}", "User-Agent": "codex-launcher/3.10.0", "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)