Compare commits
22 Commits
946f871762
...
v3.10.10
110
CHANGELOG.md
110
CHANGELOG.md
@@ -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**
|
||||
|
||||
38
README.md
38
README.md
@@ -23,7 +23,7 @@ If you want fork it, use the Github copy, here it is:
|
||||
|
||||
<p align="center">
|
||||
<strong>Run OpenAI Codex CLI & Desktop with <em>any</em> AI provider.</strong><br/>
|
||||
Google Antigravity • Gemini CLI • OpenCode • Z.AI • Anthropic • Command Code • Codebuff • OpenRouter • Crof.ai • NVIDIA NIM • OpenAdapter • Kilo.ai • DeepSeek • and more
|
||||
Google Antigravity • Gemini CLI • OpenCode • Z.AI • Anthropic • Command Code • Freebuff • OpenRouter • Crof.ai • NVIDIA NIM • OpenAdapter • Kilo.ai • DeepSeek • 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
5726
codex-launcher-gui
Executable file
File diff suppressed because it is too large
Load Diff
3292
codex-launcher-gui.py
Normal file
3292
codex-launcher-gui.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
codex-launcher_3.10.10_all.deb
Normal file
BIN
codex-launcher_3.10.10_all.deb
Normal file
Binary file not shown.
Binary file not shown.
BIN
codex-launcher_3.10.9_all.deb
Normal file
BIN
codex-launcher_3.10.9_all.deb
Normal file
Binary file not shown.
@@ -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"],
|
||||
@@ -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
101
src/cleanup-codex-stale.py
Normal 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()
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
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
1015
translate-proxy.py
1015
translate-proxy.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user