22 Commits

15 changed files with 13734 additions and 316 deletions

View File

@@ -1,5 +1,115 @@
# Changelog
## v3.10.10 (2026-05-25)
**Context Normalizer Fix — Compaction Summary Preservation**
### Bug Fixes
- Fixed normalizer stripping ALL context on resumed sessions after compaction
- Normalizer no longer auto-resets when compaction summary is present
- Compaction summaries ("Auto-compacted: N earlier turns") are always preserved
- Deduplicates consecutive identical `<goal_context>` messages (10→1)
- Emergency reset now preserves compaction summaries
- Previous behavior: after compaction reduced 1925→185 items, normalizer saw `n_tool_outputs == 0` and stripped to just `system + latest_user`, losing all context — model responded with "I don't have context"
### hashlib Fix (v3.10.9 hotfix)
- `_antigravity_normalize_context` crashed with `NameError: hashlib` on resumed sessions
- Replaced SHA256 duplicate detection with string comparison
## v3.10.9 (2026-05-25)
**Antigravity Overhaul — Context Normalizer, Claude Thinking Fix, Endpoint Lockdown**
### Antigravity Endpoint Lockdown
- Production-only: `cloudcode-pa.googleapis.com` by default
- Sandbox/staging blocked unless `ALLOW_ANTIGRAVITY_STAGING=1`
- 403 SERVICE_DISABLED falls through, 429 returns to client
### AntigravityContextNormalizer
- Bounded context — no more 136-item polluted requests for "hi"
- Simple message detector, auto-reset polluted context
- Duplicate removal, tool output budget, hard char limits
### Claude Thinking Fix (Antigravity-only)
- Fixed 400 error: `maxOutputTokens=64000` when thinking enabled
- Snake_case config, VALIDATED toolConfig, proper budgets
### z.ai / OpenRouter (cobra91 PR #4)
- Full OpenClaw attribution headers, OpenRouter caching
## v3.10.8 (2026-05-25)
**OAuth & Antigravity Endpoint Fixes**
### Re-OAuth Buttons Fixed
- Linux GUI: `load_oauth_secrets()` was undefined — buttons crashed silently on click
- Now loads OAuth secrets inline from `~/.config/codex-launcher/oauth-secrets.json`
- Both Linux and Windows Re-OAuth use PKCE + localhost callback (was deprecated OOB paste)
### Antigravity Staging/Sandbox Blocked by Default
- Proxy: production `cloudcode-pa.googleapis.com` tried FIRST, sandbox/daily/autopush as fallback only
- Proxy: 403 SERVICE_DISABLED now falls through to next endpoint instead of returning error immediately
- Project discovery: validates against production endpoint, not staging-cloudaicompanion.sandbox
- Antigravity preset `base_url` changed to production (was `daily-cloudcode-pa.sandbox.googleapis.com`)
- `[antigravity-endpoint]` log line shows which endpoints are being tried
### Other Fixes
- GLib.idle_add lambda returning truthy tuple fixed (caused repeated callbacks)
- Windows GUI project discovery also uses production endpoint
## v3.10.7 (2026-05-25)
**Prompt Enhancer — Fix Lost Context After Compaction**
### Prompt Enhancer (Per-Provider Toggle)
- **Offline mode**: Injects structured XML instructions before every user prompt to keep the model focused, decisive, and context-aware after compaction strips conversation history
- **AI-powered mode**: Optionally calls an external LLM (configurable model/URL/key) to rewrite vague prompts into clear, actionable instructions
- Prevents the "had to resend and reword" problem in long sessions where compaction summarizes hundreds of turns
- **Per-endpoint setting** — enable/disable for each provider independently
- Configurable in both Linux and Windows GUI: toggle switch, mode selector, enhancer model, URL, API key fields
### How It Works
- **Offline**: Prepends a `<prompt-enhancer>` block with rules like "never ask for clarification, infer from compacted context, execute decisively"
- **AI-powered**: Sends the user's prompt + compaction summary to a separate model (e.g. DeepSeek V4 Flash via Freebuff) which rewrites it for clarity, then prepends the offline instructions too
- Both modes run after compaction but before the request is sent upstream
## v3.10.6 (2026-05-25)
**Freebuff Integration + Codebuff OAuth Fix + Windows Consolidation**
### Freebuff (Free DeepSeek/Kimi)
- **Freebuff integration**: Free DeepSeek/Kimi models via codebuff.com API
- Fixed User-Agent to match official SDK: `ai-sdk/openai-compatible/1.0.25/codebuff`
- Fixed metadata fields: `freebuff_instance_id` + `client_id` (base36 random) + `cost_mode: "free"`
- Fixed session endpoint: POST empty `{}` body (not `{"model": model}`)
- GUI preset aliases: "Freebuff (Free DeepSeek/Kimi)", "FreeBuff", "Codebuff (Free DeepSeek/Kimi)" all map to same backend
### Codebuff Fix
- Fixed Codebuff OAuth: use `www.codebuff.com` (bare `codebuff.com` returns 307 redirect)
### OAuth Secrets & Credentials (All Providers)
- **OAuth Secrets dialog now shows ALL providers**: Google (Antigravity + Gemini CLI) AND Freebuff/Codebuff
- **Re-OAuth buttons** for each provider: instantly 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
- Editable auth token and fingerprint fields for Freebuff/Codebuff
### Windows
- Windows GUI files consolidated into `src/` (merged by cobra91 via PR #1 and PR #2)
### Proxy & GUI Improvements (cobra91 PR #3)
- CROF adaptive logic gated to `crof.ai` only — no more log pollution for other providers
- Data directory consolidation: all data now in `codex-proxy/` (was split across `codex-desktop/`, `codex-launcher/`, `codex-proxy/`)
- Sticky proxy port: persists in `.last-proxy-port`, reused on restart so Codex Desktop keeps connection
- 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)
**Windows GUI + Context Compaction for Antigravity/Gemini OAuth**

View File

@@ -23,7 +23,7 @@ If you want fork it, use the Github copy, here it is:
<p align="center">
<strong>Run OpenAI Codex CLI &amp; Desktop with <em>any</em> AI provider.</strong><br/>
Google Antigravity &bull; Gemini CLI &bull; OpenCode &bull; Z.AI &bull; Anthropic &bull; Command Code &bull; Codebuff &bull; OpenRouter &bull; Crof.ai &bull; NVIDIA NIM &bull; OpenAdapter &bull; Kilo.ai &bull; DeepSeek &bull; and more
Google Antigravity &bull; Gemini CLI &bull; OpenCode &bull; Z.AI &bull; Anthropic &bull; Command Code &bull; Freebuff &bull; OpenRouter &bull; Crof.ai &bull; NVIDIA NIM &bull; OpenAdapter &bull; Kilo.ai &bull; DeepSeek &bull; and more
</p>
<p align="center">
@@ -630,7 +630,7 @@ curl http://127.0.0.1:PORT/v1/accounts
| OpenCode Zen | OpenAI-compat | `https://opencode.ai/zen/v1` |
| OpenCode Go | OpenAI-compat | `https://opencode.ai/zen/go/v1` |
| Command Code | Command Code | `https://api.commandcode.ai` |
| **Codebuff** | **Codebuff** | `https://codebuff.com` *(free DeepSeek/Kimi — OAuth login built-in)* |
| **Codebuff / Freebuff** | **Codebuff** | `https://www.codebuff.com` *(free DeepSeek/Kimi — OAuth login built-in)* |
| Crof.ai | OpenAI-compat | `https://crof.ai/v1` |
| OpenAdapter | OpenAI-compat | `https://api.openadapter.in/v1` |
| Z.ai Coding | OpenAI-compat | `https://api.z.ai/api/coding/paas/v4` |
@@ -643,14 +643,14 @@ curl http://127.0.0.1:PORT/v1/accounts
| Google Antigravity (OAuth) | Antigravity OAuth | `daily-cloudcode-pa.sandbox.googleapis.com` |
| Custom | Any | User-defined |
### Free Models (via Codebuff)
Codebuff provides free access to these models — no API key needed:
### Free Models (via Codebuff/Freebuff)
Codebuff/Freebuff provides free access to these models — no API key needed:
- **DeepSeek V4 Pro** — Smartest model
- **DeepSeek V4 Flash** — Most efficient
- **Kimi K2.6** — Balanced
- **MiniMax M2.7** — Fastest
*Requires: `codebuff login` via GUI OAuth button, or `npm install -g codebuff && codebuff login` (GitHub OAuth)*
*Requires: `freebuff login` via GUI OAuth button, or `npm install -g freebuff && freebuff login` (GitHub OAuth)*
---
@@ -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**.
<p align="center">
<sub>
@@ -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
---

5726
codex-launcher-gui Executable file

File diff suppressed because it is too large Load Diff

3292
codex-launcher-gui.py Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -41,8 +41,8 @@ if IS_WINDOWS:
PROXY_CONFIG_DIR = _LOCAL_APPDATA / "codex-proxy"
CONFIG_DIR = HOME / ".codex"
BIN_DIR = _LOCAL_APPDATA / "Programs" / "Codex-Launcher"
LOG_DIR = _LOCAL_APPDATA / "codex-desktop"
PID_REGISTRY = _LOCAL_APPDATA / "codex-launcher" / "pids.json"
LOG_DIR = _LOCAL_APPDATA / "codex-proxy"
PID_REGISTRY = _LOCAL_APPDATA / "codex-proxy" / "pids.json"
_USAGE_STATS_FILE = _LOCAL_APPDATA / "codex-proxy" / "usage-stats.json"
MONITORING_FILE = _LOCAL_APPDATA / "codex-proxy" / "monitoring-config.json"
INCIDENT_STORE_FILE = _LOCAL_APPDATA / "codex-proxy" / "incident-store.json"
@@ -52,8 +52,8 @@ else:
PROXY_CONFIG_DIR = HOME / ".cache/codex-proxy"
CONFIG_DIR = HOME / ".codex"
BIN_DIR = HOME / ".local/bin"
LOG_DIR = HOME / ".cache/codex-desktop"
PID_REGISTRY = HOME / ".cache" / "codex-launcher" / "pids.json"
LOG_DIR = HOME / ".cache/codex-proxy"
PID_REGISTRY = HOME / ".cache/codex-proxy" / "pids.json"
_USAGE_STATS_FILE = HOME / ".cache/codex-proxy/usage-stats.json"
MONITORING_FILE = HOME / ".cache/codex-proxy/monitoring-config.json"
INCIDENT_STORE_FILE = HOME / ".cache/codex-proxy/incident-store.json"
@@ -83,6 +83,52 @@ model_catalog_json = ""
"""
CHANGELOG = [
("3.10.9", "2026-05-25", [
"Antigravity: production-only endpoints (cloudcode-pa.googleapis.com), sandbox blocked unless ALLOW_ANTIGRAVITY_STAGING=1",
"Antigravity: 403 SERVICE_DISABLED falls through, 429 returns to client (no sandbox fallback)",
"AntigravityContextNormalizer: bounded context — simple messages send minimal payload",
"Simple message detector: 'hi' etc sends only user message, no tool history",
"Auto-reset polluted context: 200+ items with simple message resets to minimal",
"Duplicate user message removal, tool output budget (max 2 verbatim, rest summarized)",
"Hard limits: 20 contents, 120K/250K/500K char budgets",
"Claude thinking fix: maxOutputTokens=64000, snake_case thinking config, VALIDATED toolConfig",
"Claude budgets: low=8192, medium=16384, high=32768",
"All fixes scoped to OAUTH_PROVIDER==google-antigravity only",
"Project discovery uses production endpoint (not staging)",
"z.ai: full OpenClaw attribution headers (cobra91 PR #4)",
"OpenRouter: X-OpenRouter-Cache header (cobra91 PR #4)",
"Fix Linux Re-OAuth: load_oauth_secrets() was undefined",
"Fix GLib.idle_add lambda returning truthy tuple",
]),
("3.10.7", "2026-05-25", [
"Prompt Enhancer: per-provider toggle to improve prompt clarity after compaction",
"Two modes: offline (template injection) and ai-powered (external LLM rewrites)",
"Offline mode: injects structured instructions to keep model focused post-compaction",
"AI-powered mode: uses configurable model/URL/key to rewrite prompts for clarity",
"Linux/Windows GUI: Prompt Enhancer switch + mode selector + model/URL/key fields",
"Prevents lost context issues in long sessions with aggressive compaction",
]),
("3.10.6", "2026-05-25", [
"Freebuff integration: free DeepSeek/Kimi via codebuff.com API",
"Fixed Freebuff User-Agent to match official SDK (ai-sdk/openai-compatible/1.0.25/codebuff)",
"Fixed Freebuff metadata: freebuff_instance_id + client_id (base36) + cost_mode: free",
"Fixed Codebuff OAuth: use www.codebuff.com (307 redirect on bare domain)",
"GUI preset aliases: Freebuff, FreeBuff, Codebuff all map to same backend",
"Windows GUI consolidated into src/ (merged by cobra91)",
"CROF adaptive logic gated to crof.ai only — no log pollution for other providers",
"Data dir consolidation: all data in codex-proxy/",
"Sticky proxy port: persists in .last-proxy-port for restart persistence",
"Adaptive compact budget raised 60% to 80% for large-context models",
"Config cleanup fix: stale proxy-*.json moved after _init_runtime()",
"Windows GUI: Clear Log, Restart Proxy, View Log buttons (cobra91 PR #3)",
"OAuth Secrets dialog shows all providers: Google + Freebuff/Codebuff",
"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",
"Aggressive compaction policies at 60% of model context limit",
@@ -410,7 +456,7 @@ PROVIDER_PRESETS = {
},
"Google Antigravity (OAuth)": {
"backend_type": "gemini-oauth-antigravity",
"base_url": "https://daily-cloudcode-pa.sandbox.googleapis.com",
"base_url": "https://cloudcode-pa.googleapis.com",
"oauth_provider": "google-antigravity",
"models": [
"antigravity-gemini-3-flash",
@@ -1388,9 +1434,18 @@ def safe_cleanup_owned(logfn=None):
_proxy_proc = None
_proxy_port = None
_PROXY_PORT_FILE = PROXY_CONFIG_DIR / ".last-proxy-port"
def _pick_free_port():
saved = None
try:
saved = int(_PROXY_PORT_FILE.read_text().strip())
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", saved))
return saved
except (ValueError, OSError, FileNotFoundError):
pass
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
@@ -1421,6 +1476,8 @@ def start_proxy_for(endpoint, logfn):
stop_proxy()
port = _pick_free_port()
_proxy_port = port
_PROXY_PORT_FILE.parent.mkdir(parents=True, exist_ok=True)
_PROXY_PORT_FILE.write_text(str(port))
model_list = endpoint.get("models", [])
if (endpoint.get("backend_type") or "").startswith("gemini-oauth") and (endpoint.get("oauth_provider") or "").startswith("google"):
@@ -1507,6 +1564,8 @@ def start_bgp_proxy(pool, model, logfn):
stop_proxy()
port = _pick_free_port()
_proxy_port = port
_PROXY_PORT_FILE.parent.mkdir(parents=True, exist_ok=True)
_PROXY_PORT_FILE.write_text(str(port))
bgp_ep = {
"name": pool["name"],

View File

@@ -3,11 +3,11 @@ set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
if [ -f "$SCRIPT_DIR/codex-launcher_3.10.5_all.deb" ]; then
echo "Installing codex-launcher_3.10.5_all.deb ..."
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.5_all.deb"
if [ -f "$SCRIPT_DIR/codex-launcher_3.10.10_all.deb" ]; then
echo "Installing codex-launcher_3.10.10_all.deb ..."
sudo dpkg -i "$SCRIPT_DIR/codex-launcher_3.10.10_all.deb"
echo ""
echo "Installed v3.10.5 via .deb package."
echo "Installed v3.10.10 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"

101
src/cleanup-codex-stale.py Normal file
View File

@@ -0,0 +1,101 @@
#!/usr/bin/env python3
"""Cleanup stale Codex Launcher processes and artifacts — cross-platform.
Kills registered process groups and removes stale PID/socket files left
by previous Codex Launcher sessions.
Windows: uses taskkill /F /T /PID
Linux: uses kill -TERM -- -PGID
"""
import json, os, sys, subprocess, time
from pathlib import Path
IS_WINDOWS = sys.platform == "win32"
if IS_WINDOWS:
_local = os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData" / "Local"))
PID_REGISTRY = Path(_local) / "codex-proxy" / "pids.json"
CODEX_DIR = Path.home() / ".codex"
_local_share = Path(_local)
_cache = Path(_local)
else:
PID_REGISTRY = Path.home() / ".cache" / "codex-proxy" / "pids.json"
CODEX_DIR = Path.home() / ".codex"
_local_share = Path.home() / ".local" / "share"
_cache = Path.home() / ".cache"
def kill_group(pid):
if IS_WINDOWS:
subprocess.run(["taskkill", "/F", "/T", "/PID", str(pid)],
capture_output=True, timeout=10)
else:
import signal
try:
pgid = os.getpgid(pid)
os.killpg(pgid, signal.SIGTERM)
time.sleep(0.5)
try:
os.killpg(pgid, signal.SIGKILL)
except OSError:
pass
except OSError:
pass
def main():
print("[cleanup] Cleaning up stale Codex Launcher processes...", file=sys.stderr)
if PID_REGISTRY.exists():
try:
with open(PID_REGISTRY) as f:
registry = json.load(f)
except Exception as e:
print(f"[cleanup] Failed to read PID registry: {e}", file=sys.stderr)
registry = {}
for kind, info in registry.items():
pid = info.get("pid") if isinstance(info, dict) else info
if pid and isinstance(pid, int):
print(f"[cleanup] Killing {kind} (PID {pid})", file=sys.stderr)
kill_group(pid)
try:
PID_REGISTRY.unlink()
except OSError:
pass
else:
print("[cleanup] No PID registry found — nothing to stop", file=sys.stderr)
stale_files = []
if IS_WINDOWS:
stale_files = [
_cache / "codex-desktop" / ".codex-desktop-pid",
_cache / "codex-desktop" / ".webview-pid",
]
else:
stale_files = [
CODEX_DIR / ".launch-action-socket",
CODEX_DIR / ".codex-desktop-launch-action",
CODEX_DIR / ".codex-desktop-pid",
CODEX_DIR / ".webview-pid",
_local_share / "codex-desktop" / ".codex-desktop-pid",
_local_share / "codex-desktop" / ".webview-pid",
_cache / "codex-desktop" / ".codex-desktop-pid",
_cache / "codex-desktop" / ".webview-pid",
]
for fp in stale_files:
try:
if fp.exists():
fp.unlink()
print(f"[cleanup] Removed {fp}", file=sys.stderr)
except OSError:
pass
print("[cleanup] Done", file=sys.stderr)
if __name__ == "__main__":
main()

View File

@@ -1798,7 +1798,7 @@ class LauncherWin(Gtk.Window):
# header row
hdr = Gtk.Box(spacing=8)
vbox.pack_start(hdr, False, False, 0)
lbl = Gtk.Label(label="<b>Codex Launcher v3.10.5</b>")
lbl = Gtk.Label(label="<b>Codex Launcher v3.10.7</b>")
lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0)
changelog_btn = Gtk.Button(label="Changelog")
@@ -1977,6 +1977,13 @@ class LauncherWin(Gtk.Window):
assist_btn.connect("clicked", lambda b: self._open_assistant())
assist_btn.set_tooltip_text("Open AI coding assistant with streaming, tools, and session management")
bb.pack_start(assist_btn, False, False, 0)
self._clear_log_btn = Gtk.Button(label="Clear Log")
self._clear_log_btn.connect("clicked", lambda b: self._buf.set_text(""))
bb.pack_start(self._clear_log_btn, False, False, 0)
self._restart_btn = Gtk.Button(label="Restart Proxy")
self._restart_btn.connect("clicked", lambda b: self._manual_restart_proxy())
self._restart_btn.set_sensitive(False)
bb.pack_start(self._restart_btn, False, False, 0)
self._kill_btn = Gtk.Button(label="Kill && Cleanup")
self._kill_btn.connect("clicked", lambda b: self._kill())
self._kill_btn.set_sensitive(False)
@@ -2073,6 +2080,7 @@ class LauncherWin(Gtk.Window):
self._btn_codex_desktop.set_sensitive(not busy and has_desk)
self._btn_codex_cli.set_sensitive(not busy and has_cli)
self._kill_btn.set_sensitive(busy)
self._restart_btn.set_sensitive(busy)
GLib.idle_add(_update)
def _rebuild_combo(self):
@@ -2206,16 +2214,32 @@ class LauncherWin(Gtk.Window):
GLib.idle_add(self.log, f"[AI Monitor] Alert: {action} (trigger: {trigger})")
def _restart_proxy_from_watcher(self):
try:
ep_name = load_endpoints().get("default")
if not ep_name:
return
for ep in load_endpoints().get("endpoints", []):
if ep.get("name") == ep_name:
self._start_proxy(ep)
break
except Exception as e:
self.log(f"[AI Monitor] Proxy restart failed: {e}")
try:
ep_name = load_endpoints().get("default")
if not ep_name:
return
for ep in load_endpoints().get("endpoints", []):
if ep.get("name") == ep_name:
self._start_proxy(ep)
break
except Exception as e:
self.log(f"[AI Monitor] Proxy restart failed: {e}")
def _manual_restart_proxy(self):
self._kill()
time.sleep(1)
try:
ep_name = load_endpoints().get("default")
if not ep_name:
self.log("No default endpoint set")
return
for ep in load_endpoints().get("endpoints", []):
if ep.get("name") == ep_name:
self._start_proxy(ep)
self.log("Proxy restarted")
break
except Exception as e:
self.log(f"Proxy restart failed: {e}")
def _open_usage(self):
try:
@@ -2808,6 +2832,154 @@ class LauncherWin(Gtk.Window):
_stop_proxy()
Gtk.main_quit()
def _google_reoauth(self, provider):
secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
try:
with open(secrets_path) as f:
secrets = json.load(f)
except Exception:
secrets = {}
is_antigravity = provider == "google-antigravity"
sec_key = "antigravity" if is_antigravity else "gemini_cli"
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:
self._show_error_dialog("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 = os.path.expanduser(f"~/.cache/codex-proxy/{token_file}")
redirect = "urn:ietf:wg:oauth:2.0:oob"
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('https://www.googleapis.com/auth/cloud-platform')}"
f"&access_type=offline&prompt=consent")
webbrowser.open(auth_url)
code_dlg = Gtk.Dialog(title=f"Re-OAuth: {'Antigravity' if is_antigravity else 'Gemini CLI'}", parent=self, modal=True)
code_dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
code_dlg.add_button("Exchange", Gtk.ResponseType.OK)
code_dlg.set_default_size(500, 180)
ca = code_dlg.get_content_area()
ca.set_margin_start(12)
ca.set_margin_end(12)
ca.set_spacing(6)
ca.pack_start(Gtk.Label(label="Browser opened for Google OAuth.\nPaste the authorization code below:", xalign=0), False, False, 0)
code_entry = Gtk.Entry()
code_entry.set_placeholder_text("4/0AX...")
ca.pack_start(code_entry, False, False, 4)
ca.show_all()
if code_dlg.run() == Gtk.ResponseType.OK:
code = code_entry.get_text().strip()
if code:
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()
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 → {token_path}")
except Exception as e:
self._show_error_dialog("Token exchange failed", str(e)[:300])
code_dlg.destroy()
def _codebuff_reoauth(self):
self._codebuff_oauth_standalone()
def _codebuff_oauth_standalone(self):
import uuid
dlg = Gtk.Dialog(title="Freebuff / Codebuff Login", parent=self, modal=True)
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
dlg.set_default_size(500, 240)
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(8)
area.pack_start(Gtk.Label(label="<b>Sign in with GitHub via Codebuff</b>", use_markup=True, xalign=0), False, False, 0)
status_lbl = Gtk.Label(label="Requesting login URL…", xalign=0)
status_lbl.set_line_wrap(True)
status_lbl.set_max_width_chars(60)
area.pack_start(status_lbl, False, False, 4)
link_lbl = Gtk.Label(xalign=0)
link_lbl.set_line_wrap(True)
link_lbl.set_max_width_chars(60)
area.pack_start(link_lbl, False, False, 4)
spinner = Gtk.Spinner()
spinner.start()
area.pack_start(spinner, False, False, 8)
area.show_all()
link_lbl.set_visible(False)
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": "codex-launcher/3.10.7"})
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"
GLib.idle_add(_done)
return
GLib.idle_add(lambda: (status_lbl.set_text("Open this URL in your browser:"),
link_lbl.set_markup(f'<a href="{login_url}">{login_url}</a>'),
link_lbl.set_visible(True)))
webbrowser.open(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": "codex-launcher/3.10.7"})
pd = json.loads(urllib.request.urlopen(pr, timeout=10).read())
if pd.get("user", {}).get("authToken"):
result["success"] = True
result["user"] = pd["user"]
GLib.idle_add(_done)
return
except Exception:
pass
result["error"] = "Timed out"
except Exception as e:
result["error"] = str(e)[:200]
GLib.idle_add(_done)
def _done():
spinner.stop()
if result["success"] and result["user"]:
u = result["user"]
cp = os.path.expanduser("~/.config/manicode/credentials.json")
os.makedirs(os.path.dirname(cp), 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(cp, "w") as f:
json.dump(creds, f, indent=2)
os.chmod(cp, 0o600)
status_lbl.set_text(f"Logged in as {u.get('email', 'OK')}")
link_lbl.set_visible(False)
GLib.timeout_add_seconds(2, dlg.destroy)
else:
status_lbl.set_text(f"Failed: {result.get('error', 'unknown')}")
threading.Thread(target=_thread, daemon=True).start()
dlg.connect("response", lambda d, r: d.destroy())
dlg.run()
def _edit_oauth_secrets(self):
secrets_path = os.path.expanduser("~/.config/codex-launcher/oauth-secrets.json")
try:
@@ -2817,10 +2989,10 @@ class LauncherWin(Gtk.Window):
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 = Gtk.Dialog(title="OAuth Secrets & Credentials", parent=self, modal=True)
dlg.add_button("Cancel", Gtk.ResponseType.CANCEL)
dlg.add_button("Save", Gtk.ResponseType.OK)
dlg.set_default_size(540, 420)
dlg.set_default_size(580, 650)
area = dlg.get_content_area()
area.set_margin_start(16)
area.set_margin_end(16)
@@ -2828,17 +3000,43 @@ class LauncherWin(Gtk.Window):
area.set_margin_bottom(12)
area.set_spacing(6)
area.pack_start(Gtk.Label(label="<b>Google OAuth 2.0 credentials</b>\n<small>Stored locally in ~/.config/codex-launcher/oauth-secrets.json</small>", use_markup=True, xalign=0), False, False, 4)
sw = Gtk.ScrolledWindow()
sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
sw.add(vbox)
area.pack_start(sw, True, True, 0)
vbox.pack_start(Gtk.Label(label="<b>Google OAuth 2.0 Client Credentials</b>\n<small>~/.config/codex-launcher/oauth-secrets.json</small>", use_markup=True, xalign=0), False, False, 4)
google_token_dir = os.path.expanduser("~/.cache/codex-proxy")
fields = {}
for section_key, section_label in [("antigravity", "Antigravity (CloudCode)"), ("gemini_cli", "Gemini CLI")]:
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"),
]:
section_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=2)
hdr_row = Gtk.Box(spacing=6)
hdr_row.pack_start(Gtk.Label(label=f"\n<b>{section_label}</b>", use_markup=True, xalign=0), True, True, 0)
reauth_btn = Gtk.Button(label="Re-OAuth")
reauth_btn.set_size_request(80, -1)
reauth_btn.connect("clicked", lambda b, p=oauth_prov: self._google_reoauth(p))
hdr_row.pack_end(reauth_btn, False, False, 0)
import_btn = Gtk.Button(label="Import JSON")
import_btn.set_size_request(100, -1)
hdr_row.pack_end(import_btn, False, False, 0)
section_box.pack_start(hdr_row, False, False, 2)
token_path = os.path.join(google_token_dir, token_file)
has_token = os.path.exists(token_path)
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
tok_status = "Token: <span foreground='#27ae60' weight='bold'>valid</span>" if has_token else "Token: <span foreground='#e67e22' weight='bold'>missing</span>"
section_box.pack_start(Gtk.Label(label=tok_status, use_markup=True, xalign=0), False, False, 0)
sec = data.get(section_key, {})
for fk, fl in [("client_id", "Client ID"), ("client_secret", "Client Secret")]:
row = Gtk.Box(spacing=6)
@@ -2846,7 +3044,7 @@ class LauncherWin(Gtk.Window):
lbl.set_size_request(100, -1)
entry = Gtk.Entry()
entry.set_text(sec.get(fk, ""))
entry.set_size_request(380, -1)
entry.set_size_request(360, -1)
if fk == "client_secret":
entry.set_visibility(False)
entry.set_invisible_char("*")
@@ -2855,10 +3053,63 @@ class LauncherWin(Gtk.Window):
section_box.pack_start(row, False, False, 2)
fields[(section_key, fk)] = entry
import_btn.connect("clicked", lambda b, sk=section_key: self._import_oauth_json(fields, sk))
area.pack_start(section_box, False, False, 0)
vbox.pack_start(section_box, False, False, 0)
area.pack_start(Gtk.Label(label="\n<small>Import a client_secret_*.json from Google Cloud Console\nor edit fields manually. console.cloud.google.com → Credentials</small>", use_markup=True, xalign=0), False, False, 4)
area.show_all()
vbox.pack_start(Gtk.Label(label="<small>Import client_secret_*.json from Google Cloud Console → Credentials</small>", use_markup=True, xalign=0), False, False, 4)
sep = Gtk.Separator()
vbox.pack_start(sep, False, False, 8)
vbox.pack_start(Gtk.Label(label="\n<b>Freebuff / Codebuff Credentials</b>\n<small>~/.config/manicode/credentials.json</small>", use_markup=True, xalign=0), False, False, 4)
cb_creds_path = os.path.expanduser("~/.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_status_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=4)
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 = "#27ae60" if has_cb_token else "#e67e22"
cb_info_lbl = Gtk.Label(label=f"{cb_info}\nStatus: <span foreground=\"{status_color}\" weight=\"bold\">{status_text}</span>", use_markup=True, xalign=0)
cb_status_box.pack_start(cb_info_lbl, False, False, 2)
for fk, fl in [("authToken", "Auth Token"), ("fingerprintId", "Fingerprint ID")]:
row = Gtk.Box(spacing=6)
lbl = Gtk.Label(label=fl + ":", xalign=0)
lbl.set_size_request(110, -1)
entry = Gtk.Entry()
entry.set_text(cb_default.get(fk, ""))
entry.set_size_request(360, -1)
entry.set_visibility(False)
entry.set_invisible_char("*")
row.pack_start(lbl, False, False, 0)
row.pack_start(entry, True, True, 0)
cb_status_box.pack_start(row, False, False, 2)
cb_fields[fk] = entry
cb_btn_row = Gtk.Box(spacing=6)
cb_login_btn = Gtk.Button(label="Re-OAuth (GitHub Login)")
cb_login_btn.connect("clicked", lambda b: self._codebuff_reoauth())
cb_btn_row.pack_start(cb_login_btn, False, False, 0)
cb_status_box.pack_start(cb_btn_row, False, False, 4)
vbox.pack_start(cb_status_box, False, False, 0)
cb_accounts = cb_data.get("accounts", [])
if cb_accounts:
vbox.pack_start(Gtk.Label(label=f"\n<small>Additional accounts: {len(cb_accounts)} (edit credentials.json manually)</small>", use_markup=True, xalign=0), False, False, 2)
vbox.show_all()
sw.show_all()
if dlg.run() == Gtk.ResponseType.OK:
for (sk, fk), entry in fields.items():
@@ -2872,6 +3123,20 @@ class LauncherWin(Gtk.Window):
os.chmod(secrets_path, 0o600)
except Exception as e:
self._show_error_dialog("Save failed", str(e))
cb_updated = dict(cb_default)
for fk, entry in cb_fields.items():
val = entry.get_text().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)
os.chmod(cb_creds_path, 0o600)
except Exception as e:
self._show_error_dialog("Save failed", str(e))
dlg.destroy()
def _import_oauth_json(self, fields, section_key):
@@ -3238,6 +3503,38 @@ class EditEndpointDialog(Gtk.Dialog):
add_row(7, "Effort:", self._combo_effort)
self._on_reasoning_toggled()
enhancer_box = Gtk.Box(spacing=6)
self._switch_enhancer = Gtk.Switch()
self._switch_enhancer.set_active(self._data.get("prompt_enhancer", False))
enhancer_box.pack_start(self._switch_enhancer, False, False, 0)
self._enhancer_status_lbl = Gtk.Label()
enhancer_box.pack_start(self._enhancer_status_lbl, False, False, 0)
self._switch_enhancer.connect("notify::active", lambda *a: self._on_enhancer_toggled())
self._combo_enhancer_mode = Gtk.ComboBoxText()
for mode in ["offline", "ai-powered"]:
self._combo_enhancer_mode.append(mode, mode.capitalize())
self._combo_enhancer_mode.set_active_id(self._data.get("prompt_enhancer_mode", "offline"))
enhancer_box.pack_start(self._combo_enhancer_mode, False, False, 6)
add_row(8, "Prompt Enhancer:", enhancer_box)
self._on_enhancer_toggled()
self._entry_enhancer_model = Gtk.Entry()
self._entry_enhancer_model.set_placeholder_text("e.g. deepseek/deepseek-v4-flash (ai-powered mode only)")
self._entry_enhancer_model.set_text(self._data.get("prompt_enhancer_model", ""))
add_row(9, "Enhancer Model:", self._entry_enhancer_model)
self._entry_enhancer_url = Gtk.Entry()
self._entry_enhancer_url.set_placeholder_text("e.g. https://www.codebuff.com/api/v1 (ai-powered mode only)")
self._entry_enhancer_url.set_text(self._data.get("prompt_enhancer_url", ""))
add_row(10, "Enhancer URL:", self._entry_enhancer_url)
self._entry_enhancer_key = Gtk.Entry()
self._entry_enhancer_key.set_placeholder_text("API key for enhancer model (ai-powered mode only)")
self._entry_enhancer_key.set_text(self._data.get("prompt_enhancer_key", ""))
self._entry_enhancer_key.set_visibility(False)
self._entry_enhancer_key.set_invisible_char("*")
add_row(11, "Enhancer Key:", self._entry_enhancer_key)
# Models
mlbl = Gtk.Label(label="Models:", xalign=0)
area.pack_start(mlbl, False, False, 4)
@@ -3377,6 +3674,13 @@ class EditEndpointDialog(Gtk.Dialog):
else:
self._lbl_reasoning.set_markup('<span foreground="#e67e22" weight="bold">OFF</span>')
def _on_enhancer_toggled(self, *_):
active = self._switch_enhancer.get_active()
if active:
self._enhancer_status_lbl.set_markup('<span foreground="#27ae60" weight="bold">ON</span>')
else:
self._enhancer_status_lbl.set_markup('<span foreground="#888888" weight="bold">OFF</span>')
def _do_oauth_login(self):
preset_name = self._combo_preset.get_active_text() or "Custom"
preset = PROVIDER_PRESETS.get(preset_name, {})
@@ -3706,7 +4010,7 @@ class EditEndpointDialog(Gtk.Dialog):
auth_url = "https://www.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.5"})
headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.10.7"})
resp = urllib.request.urlopen(req, timeout=30)
data = json.loads(resp.read())
login_url = data.get("loginUrl", "") or data.get("login_url", "")
@@ -3731,7 +4035,7 @@ class EditEndpointDialog(Gtk.Dialog):
time.sleep(2)
try:
poll_req = urllib.request.Request(poll_url,
headers={"User-Agent": "codex-launcher/3.10.5"})
headers={"User-Agent": "codex-launcher/3.10.7"})
poll_resp = urllib.request.urlopen(poll_req, timeout=10)
poll_data = json.loads(poll_resp.read())
user = poll_data.get("user")
@@ -3930,6 +4234,17 @@ class EditEndpointDialog(Gtk.Dialog):
new_ep["cc_version"] = cc_ver
new_ep["reasoning_enabled"] = self._switch_reasoning.get_active()
new_ep["reasoning_effort"] = self._combo_effort.get_active_id() or "medium"
new_ep["prompt_enhancer"] = self._switch_enhancer.get_active()
new_ep["prompt_enhancer_mode"] = self._combo_enhancer_mode.get_active_id() or "offline"
enh_model = self._entry_enhancer_model.get_text().strip()
enh_url = self._entry_enhancer_url.get_text().strip()
enh_key = self._entry_enhancer_key.get_text().strip()
if enh_model:
new_ep["prompt_enhancer_model"] = enh_model
if enh_url:
new_ep["prompt_enhancer_url"] = enh_url
if enh_key:
new_ep["prompt_enhancer_key"] = enh_key
preset_name = self._combo_preset.get_active_text() or "Custom"
preset = PROVIDER_PRESETS.get(preset_name, {})
if preset.get("oauth_provider"):

View File

@@ -225,6 +225,30 @@ class EditEndpointDialog:
add_field("Reasoning:", lambda: reason_frame)
self._on_reasoning_toggled()
enhancer_frame = ttk.Frame(grid)
self._enhancer_var = tk.BooleanVar(value=self._data.get("prompt_enhancer", False))
self._enhancer_cb = ttk.Checkbutton(enhancer_frame, text="Prompt Enhancer", variable=self._enhancer_var, command=self._on_enhancer_toggled)
self._enhancer_cb.pack(side="left")
self._enhancer_status_lbl = ttk.Label(enhancer_frame, text="", foreground="gray")
self._enhancer_status_lbl.pack(side="left", padx=(6, 0))
self._enhancer_mode = ttk.Combobox(enhancer_frame, values=["offline", "ai-powered"], state="readonly", width=10)
self._enhancer_mode.set(self._data.get("prompt_enhancer_mode", "offline"))
self._enhancer_mode.pack(side="left", padx=(8, 0))
add_field("Prompt Enhancer:", lambda: enhancer_frame)
self._on_enhancer_toggled()
self._entry_enhancer_model = ttk.Entry(grid)
self._entry_enhancer_model.insert(0, self._data.get("prompt_enhancer_model", ""))
add_field("Enhancer Model:", lambda: self._entry_enhancer_model)
self._entry_enhancer_url = ttk.Entry(grid)
self._entry_enhancer_url.insert(0, self._data.get("prompt_enhancer_url", ""))
add_field("Enhancer URL:", lambda: self._entry_enhancer_url)
self._entry_enhancer_key = ttk.Entry(grid, show="*")
self._entry_enhancer_key.insert(0, self._data.get("prompt_enhancer_key", ""))
add_field("Enhancer Key:", lambda: self._entry_enhancer_key)
grid.columnconfigure(1, weight=1)
ttk.Label(main, text="Models:").pack(anchor="w", pady=(8, 2))
@@ -236,6 +260,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))
@@ -274,6 +299,12 @@ class EditEndpointDialog:
state = "readonly" if self._reason_var.get() else "disabled"
self._combo_effort.configure(state=state)
def _on_enhancer_toggled(self):
if self._enhancer_var.get():
self._enhancer_status_lbl.configure(text="ON", foreground="#2ea043")
else:
self._enhancer_status_lbl.configure(text="OFF", foreground="#888888")
def _apply_selected_preset(self, initial=False):
preset_name = self._combo_preset.get() or "Custom"
preset = PROVIDER_PRESETS.get(preset_name, {})
@@ -298,6 +329,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 +424,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 +617,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()
@@ -615,10 +743,21 @@ class EditEndpointDialog:
"provider_preset": self._combo_preset.get() or "Custom",
"reasoning_enabled": self._reason_var.get(),
"reasoning_effort": self._combo_effort.get() or "medium",
"prompt_enhancer": self._enhancer_var.get(),
"prompt_enhancer_mode": self._enhancer_mode.get() or "offline",
}
cc_ver = self._entry_cc_ver.get().strip()
if cc_ver:
new_ep["cc_version"] = cc_ver
enh_model = self._entry_enhancer_model.get().strip()
enh_url = self._entry_enhancer_url.get().strip()
enh_key = self._entry_enhancer_key.get().strip()
if enh_model:
new_ep["prompt_enhancer_model"] = enh_model
if enh_url:
new_ep["prompt_enhancer_url"] = enh_url
if enh_key:
new_ep["prompt_enhancer_key"] = enh_key
preset_name = self._combo_preset.get() or "Custom"
preset = PROVIDER_PRESETS.get(preset_name, {})
if preset.get("oauth_provider"):
@@ -1938,6 +2077,61 @@ class BenchmarkWindow:
# Main Launcher Window
# ═══════════════════════════════════════════════════════════════════════
def _oauth_discover_project_win(access_token, token_path, tokens):
project_id = ""
try:
lr = urllib.request.Request(
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
data=json.dumps({}).encode(),
headers={"Content-Type": "application/json",
"Authorization": f"Bearer {access_token}",
"User-Agent": "google-api-nodejs-client/9.15.1"})
lresp = urllib.request.urlopen(lr, timeout=15)
ldata = json.loads(lresp.read())
p = ldata.get("cloudaicompanionProject", "")
if isinstance(p, dict):
project_id = p.get("id", "")
elif isinstance(p, str):
project_id = p
except Exception:
pass
if not project_id:
return ""
try:
test_url = f"https://cloudcode-pa.googleapis.com/v1internal:listModels?project={project_id}"
test_req = urllib.request.Request(test_url,
headers={"Authorization": f"Bearer {access_token}",
"User-Agent": "google-api-nodejs-client/9.15.1"})
urllib.request.urlopen(test_req, timeout=10)
except urllib.error.HTTPError as e:
if e.code == 403 and "SERVICE_DISABLED" in (e.read().decode()[:500]):
try:
list_req = urllib.request.Request(
"https://cloudresourcemanager.googleapis.com/v1/projects?filter=lifecycleState:ACTIVE",
headers={"Authorization": f"Bearer {access_token}"})
list_resp = urllib.request.urlopen(list_req, timeout=15)
projects = json.loads(list_resp.read()).get("projects", [])
for proj in projects:
pid = proj.get("projectId", "")
if not pid or pid == project_id:
continue
try:
t2 = urllib.request.Request(
f"https://cloudcode-pa.googleapis.com/v1internal:listModels?project={pid}",
headers={"Authorization": f"Bearer {access_token}",
"User-Agent": "google-api-nodejs-client/9.15.1"})
urllib.request.urlopen(t2, timeout=10)
project_id = pid
break
except Exception:
continue
except Exception:
pass
tokens["project_id"] = project_id
with open(token_path, "w") as f:
json.dump(tokens, f, indent=2)
return project_id
class LauncherWin:
def __init__(self, root):
self._root = root
@@ -2058,10 +2252,13 @@ class LauncherWin:
# Bottom bar
bb = ttk.Frame(main)
bb.pack(fill="x", pady=(6, 0))
ttk.Button(bb, text="AI Assistant", command=self._open_assistant).pack(side="left")
ttk.Button(bb, text="Clear Log", command=self._clear_log).pack(side="left")
self._restart_btn = ttk.Button(bb, text="Restart Proxy", command=self._restart_proxy, state="disabled")
self._restart_btn.pack(side="left", padx=(4, 0))
ttk.Button(bb, text="AI Assistant", command=self._open_assistant).pack(side="left", padx=(4, 0))
self._kill_btn = ttk.Button(bb, text="Kill && Cleanup", command=self._kill, state="disabled")
self._kill_btn.pack(side="left", fill="x", expand=True, padx=(8, 0))
ttk.Button(bb, text="View Log", command=lambda: open_file(str(LAUNCH_LOG))).pack(side="left")
ttk.Button(bb, text="View Log", command=self._open_proxy_log_dir).pack(side="left")
ttk.Button(bb, text="Close", command=self._do_close).pack(side="left", padx=(8, 0))
self._rebuild_combo()
@@ -2079,6 +2276,25 @@ class LauncherWin:
self._log_text.see("end")
self._log_text.configure(state="disabled")
def _clear_log(self):
self._log_text.configure(state="normal")
self._log_text.delete("1.0", "end")
self._log_text.configure(state="disabled")
def _restart_proxy(self):
self._kill()
ep_name = load_endpoints().get("default")
if not ep_name:
self.log("No default endpoint set.")
return
for ep in load_endpoints().get("endpoints", []):
if ep.get("name") == ep_name:
time.sleep(0.3)
start_proxy_for(ep, self.log)
self.log(f"Proxy restarted for {ep_name}")
return
self.log(f"Endpoint '{ep_name}' not found.")
def _log_dependency_status(self):
if self._cli_info:
_, ver = self._cli_info
@@ -2191,45 +2407,298 @@ class LauncherWin:
def _open_benchmark(self):
BenchmarkWindow(self._root)
def _open_proxy_log_dir(self):
log_dir = str(PROXY_CONFIG_DIR)
req_log = PROXY_CONFIG_DIR / "requests.log"
if IS_WINDOWS:
if req_log.exists():
os.startfile(str(req_log))
else:
os.startfile(log_dir)
else:
import subprocess as _sp
_sp.Popen(["xdg-open", log_dir])
def _open_assistant(self):
assist_path = str(Path(__file__).resolve().parent / "flet-codex-assist.py")
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):
import http.server
is_antigravity = provider == "google-antigravity"
sec_key = "antigravity" if is_antigravity else "gemini_cli"
secrets_data = load_oauth_secrets()
sec = secrets_data.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)
provider_kind = "antigravity" if is_antigravity else "cli"
if is_antigravity:
SCOPES = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/cclog",
"https://www.googleapis.com/auth/experimentsandconfigs",
]
port = 51121
redirect_uri = f"http://localhost:{port}/oauth-callback"
else:
SCOPES = [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile",
]
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]
redirect_uri = f"http://127.0.0.1:{port}/oauth2callback"
state = secrets.token_hex(32)
verifier = secrets.token_urlsafe(64)
challenge = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode()
scope_str = " ".join(SCOPES)
auth_url = (
f"https://accounts.google.com/o/oauth2/v2/auth?"
f"client_id={CLIENT_ID}"
f"&redirect_uri={urllib.parse.quote(redirect_uri)}"
f"&response_type=code"
f"&scope={urllib.parse.quote(scope_str)}"
f"&access_type=offline"
f"&prompt=select_account%20consent"
f"&state={state}"
f"&code_challenge={challenge}"
f"&code_challenge_method=S256"
)
oauth_dlg = tk.Toplevel(parent_dlg or self._root)
oauth_dlg.title(f"Re-OAuth: {'Antigravity' if is_antigravity else 'Gemini CLI'}")
oauth_dlg.geometry("520x200")
if parent_dlg:
oauth_dlg.transient(parent_dlg)
else:
oauth_dlg.transient(self._root)
oauth_dlg.grab_set()
tk.Label(oauth_dlg, text=f"Re-authenticating {'Antigravity' if is_antigravity else 'Gemini CLI'}",
font=("Segoe UI", 11, "bold")).pack(padx=16, pady=(12, 0), anchor="w")
link_lbl = tk.Label(oauth_dlg, text="Click here to open Google authorization", fg="blue", cursor="hand2")
link_lbl.pack(padx=16, anchor="w")
link_lbl.bind("<Button-1>", lambda e: open_url(auth_url))
status_var = tk.StringVar(value="Waiting for browser callback...")
tk.Label(oauth_dlg, textvariable=status_var).pack(padx=16, pady=(8, 0), anchor="w")
code_holder = [None]
error_holder = [None]
class OAuthHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self2):
qs = urllib.parse.urlparse(self2.path).query
params = urllib.parse.parse_qs(qs)
if "code" in params:
if params.get("state", [None])[0] != state:
self2.send_response(400)
self2.end_headers()
self2.wfile.write(b"CSRF state mismatch")
error_holder[0] = "CSRF state mismatch"
return
code_holder[0] = params["code"][0]
self2.send_response(302)
self2.send_header("Location", "https://developers.google.com/gemini-code-assist/auth_success_gemini")
self2.end_headers()
else:
error_holder[0] = params.get("error", ["unknown"])[0]
self2.send_response(302)
self2.send_header("Location", "https://developers.google.com/gemini-code-assist/auth_failure_gemini")
self2.end_headers()
def log_message(self2, fmt, *args):
pass
try:
bind_host = "localhost" if is_antigravity else "127.0.0.1"
server = http.server.HTTPServer((bind_host, port), OAuthHandler)
except OSError:
status_var.set(f"Port {port} in use — close other apps and retry.")
return
def _wait():
deadline = time.time() + 120
while code_holder[0] is None and error_holder[0] is None and time.time() < deadline:
server.handle_request()
server.server_close()
if code_holder[0]:
try:
tok_data = urllib.parse.urlencode({
"code": code_holder[0], "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET,
"redirect_uri": redirect_uri, "grant_type": "authorization_code",
"code_verifier": verifier,
}).encode()
req = urllib.request.Request("https://oauth2.googleapis.com/token", data=tok_data,
headers={"Content-Type": "application/x-www-form-urlencoded"})
resp = urllib.request.urlopen(req, timeout=30)
tokens = json.loads(resp.read())
tokens["client_id"] = CLIENT_ID
tokens["client_secret"] = CLIENT_SECRET
tokens["provider_kind"] = provider_kind
tokens["expires_at"] = time.time() + tokens.get("expires_in", 3600)
os.makedirs(os.path.dirname(token_path), exist_ok=True)
with open(token_path, "w") as f:
json.dump(tokens, f, indent=2)
project_id = _oauth_discover_project_win(tokens["access_token"], token_path, tokens)
self._root.after(0, lambda: status_var.set(f"OK! Project: {project_id or 'none'}"))
self._root.after(2000, oauth_dlg.destroy)
except Exception as e:
self._root.after(0, lambda: status_var.set(f"Failed: {str(e)[:200]}"))
else:
self._root.after(0, lambda: status_var.set(f"Failed: {error_holder[0] or 'No code received'}"))
open_url(auth_url)
threading.Thread(target=_wait, daemon=True).start()
oauth_dlg.wait_window()
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
@@ -2237,7 +2706,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))
@@ -2255,6 +2767,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)
@@ -2452,6 +2978,7 @@ class LauncherWin:
self._btn_codex_desktop.configure(state="disabled" if busy or not has_desk else "normal")
self._btn_codex_cli.configure(state="disabled" if busy or not has_cli else "normal")
self._kill_btn.configure(state="normal" if busy else "disabled")
self._restart_btn.configure(state="normal" if busy else "disabled")
self._root.after(0, _update)
def _launch(self, target):

2140
src/codex_launcher_lib.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff