v3.8.3: Fix codebuff streaming — SSE events now reach Codex client
- Root cause: _handle_codebuff streaming loop collected events but never wrote them to self.wfile (stream_buffered_events was not called) - Fix: Replaced manual loop with stream_buffered_events() + on_event callback - Confirmed working: raw API streaming, non-stream, and stream through proxy - Updated CHANGELOG.md, README.md, version labels to 3.8.3
This commit is contained in:
@@ -6,7 +6,7 @@ gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib
|
||||
import subprocess, os, signal, sys, threading, time, json, urllib.request, urllib.parse, urllib.error, tempfile, shutil
|
||||
import hashlib, socket, ssl, contextlib, re, collections
|
||||
import base64, secrets
|
||||
import base64, secrets, uuid, webbrowser
|
||||
from pathlib import Path
|
||||
|
||||
HOME = Path.home()
|
||||
@@ -26,6 +26,22 @@ model_catalog_json = ""
|
||||
"""
|
||||
|
||||
CHANGELOG = [
|
||||
("3.8.3", "2026-05-24", [
|
||||
"FIXED: Codebuff streaming — SSE events now reach Codex client",
|
||||
"Root cause: stream_buffered_events was never called for codebuff",
|
||||
"Codebuff stream uses buffered flushing (30ms / 4KB / urgent)",
|
||||
"Codebuff OAuth — built-in login flow (no external CLI needed)",
|
||||
"Codebuff API: reverse-engineered www.codebuff.com endpoints",
|
||||
"Codebuff session management with instance ID (waiting room)",
|
||||
"Codebuff agent run lifecycle (start/finish) with model routing",
|
||||
"Free DeepSeek V4 Pro, V4 Flash, Kimi K2.6, MiniMax M2.7",
|
||||
"Reasoning mode works with codebuff (thinking tokens supported)",
|
||||
"GUI: Sandbox mode selector (Read-only / Workspace / Full Access)",
|
||||
"GUI: Approval mode selector (Untrusted / On Request / Full Auto)",
|
||||
"GUI: Codebuff Login button in endpoint editor",
|
||||
"Fixed _STATS undefined error in /health endpoint",
|
||||
"Fixed codebuff credential path (reads default account)",
|
||||
]),
|
||||
("3.8.1", "2026-05-24", [
|
||||
"Codebuff integration — free DeepSeek V4 Pro, V4 Flash, Kimi K2.6, MiniMax M2.7",
|
||||
"Codebuff backend: auto agent-run lifecycle, credential detection, model routing",
|
||||
@@ -321,6 +337,7 @@ PROVIDER_PRESETS = {
|
||||
"Codebuff (Free DeepSeek/Kimi)": {
|
||||
"backend_type": "codebuff",
|
||||
"base_url": "https://codebuff.com",
|
||||
"oauth_provider": "codebuff",
|
||||
"models": [
|
||||
"deepseek/deepseek-v4-pro", "deepseek/deepseek-v4-flash",
|
||||
"moonshotai/kimi-k2.6", "minimax/minimax-m2.7",
|
||||
@@ -995,6 +1012,11 @@ def safe_cleanup_owned(logfn=None):
|
||||
|
||||
def _start_proxy_for(endpoint, logfn):
|
||||
global _proxy_proc, _proxy_port
|
||||
# Clear stale Python bytecode cache so proxy picks up latest source changes
|
||||
import shutil
|
||||
pycache = os.path.join(os.path.dirname(os.path.abspath(__file__)), '__pycache__')
|
||||
if os.path.isdir(pycache):
|
||||
shutil.rmtree(pycache, ignore_errors=True)
|
||||
_stop_proxy()
|
||||
port = _pick_free_port()
|
||||
_proxy_port = port
|
||||
@@ -1684,7 +1706,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.8.1</b>")
|
||||
lbl = Gtk.Label(label="<b>Codex Launcher v3.8.3</b>")
|
||||
lbl.set_use_markup(True)
|
||||
hdr.pack_start(lbl, False, False, 0)
|
||||
changelog_btn = Gtk.Button(label="Changelog")
|
||||
@@ -1790,6 +1812,26 @@ class LauncherWin(Gtk.Window):
|
||||
self._model_combo = Gtk.ComboBoxText()
|
||||
sel_box.pack_start(self._model_combo, True, True, 0)
|
||||
|
||||
# sandbox mode selector
|
||||
sel_box.pack_start(Gtk.Label(label="Sandbox:"), False, False, 0)
|
||||
self._sandbox_combo = Gtk.ComboBoxText()
|
||||
for v, l in [("read-only", "Read-only"),
|
||||
("workspace-write", "Workspace"),
|
||||
("danger-full-access", "Full Access")]:
|
||||
self._sandbox_combo.append(v, l)
|
||||
self._sandbox_combo.set_active_id("workspace-write")
|
||||
sel_box.pack_start(self._sandbox_combo, True, True, 0)
|
||||
|
||||
# approval mode selector
|
||||
sel_box.pack_start(Gtk.Label(label="Approval:"), False, False, 0)
|
||||
self._approval_combo = Gtk.ComboBoxText()
|
||||
for v, l in [("untrusted", "Untrusted"),
|
||||
("on-request", "On Request"),
|
||||
("never", "Never (Full Auto)")]:
|
||||
self._approval_combo.append(v, l)
|
||||
self._approval_combo.set_active_id("on-request")
|
||||
sel_box.pack_start(self._approval_combo, True, True, 0)
|
||||
|
||||
# launch buttons
|
||||
btn_box = Gtk.Box(spacing=8, homogeneous=True)
|
||||
vbox.pack_start(btn_box, False, False, 8)
|
||||
@@ -2523,7 +2565,6 @@ class LauncherWin(Gtk.Window):
|
||||
"""Launch codex CLI in a terminal with the selected endpoint."""
|
||||
self.log(f"Launching Codex CLI with {ep['name']}…")
|
||||
|
||||
# Find a terminal emulator
|
||||
terms = [
|
||||
("x-terminal-emulator", ["-e"]),
|
||||
("kgx", ["--"]),
|
||||
@@ -2543,16 +2584,17 @@ class LauncherWin(Gtk.Window):
|
||||
self.log("ERROR: no terminal emulator found (tried x-terminal-emulator, kgx, gnome-terminal, konsole, xterm)")
|
||||
return
|
||||
|
||||
# For proxied endpoints, the proxy is already running (from _run)
|
||||
# For native, no proxy needed
|
||||
sandbox = self._sandbox_combo.get_active_id() or "workspace-write"
|
||||
approval = self._approval_combo.get_active_id() or "on-request"
|
||||
|
||||
cmd_parts = [term] + term_args
|
||||
|
||||
if ep["backend_type"] == "native":
|
||||
# Just run codex directly — config.toml is already set up
|
||||
cmd_parts.extend(["codex", "-c", f"model={model}"])
|
||||
cmd_parts.extend(["codex", "-c", f"model={model}",
|
||||
"-s", sandbox, "-a", approval])
|
||||
else:
|
||||
# Proxy is running, run codex with the profile
|
||||
cmd_parts.extend(["codex", "--profile", ep["name"], "-c", f"model={model}"])
|
||||
cmd_parts.extend(["codex", "--profile", ep["name"], "-c", f"model={model}",
|
||||
"-s", sandbox, "-a", approval])
|
||||
|
||||
self.log(f"Running: {' '.join(cmd_parts)}")
|
||||
self._proc = subprocess.Popen(cmd_parts, preexec_fn=os.setsid)
|
||||
@@ -2618,7 +2660,9 @@ class LauncherWin(Gtk.Window):
|
||||
self.log("ERROR: no terminal emulator found")
|
||||
return
|
||||
|
||||
cmd_parts = [term] + term_args + ["codex"]
|
||||
sandbox = self._sandbox_combo.get_active_id() or "workspace-write"
|
||||
approval = self._approval_combo.get_active_id() or "on-request"
|
||||
cmd_parts = [term] + term_args + ["codex", "-s", sandbox, "-a", approval]
|
||||
self.log(f"Running: {' '.join(cmd_parts)}")
|
||||
self._proc = subprocess.Popen(cmd_parts, preexec_fn=os.setsid)
|
||||
pid = self._proc.pid
|
||||
@@ -3097,9 +3141,14 @@ class EditEndpointDialog(Gtk.Dialog):
|
||||
def _apply_selected_preset(self, initial=False):
|
||||
preset_name = self._combo_preset.get_active_text() or "Custom"
|
||||
preset = PROVIDER_PRESETS.get(preset_name, PROVIDER_PRESETS["Custom"])
|
||||
is_oauth = bool(preset.get("oauth_provider"))
|
||||
oauth_provider = preset.get("oauth_provider", "")
|
||||
is_oauth = bool(oauth_provider)
|
||||
self._oauth_btn.set_visible(is_oauth)
|
||||
if is_oauth:
|
||||
if oauth_provider == "codebuff":
|
||||
self._oauth_btn.set_label("Codebuff Login")
|
||||
self._entry_key.set_placeholder_text("Auto-filled by codebuff login")
|
||||
elif is_oauth:
|
||||
self._oauth_btn.set_label("OAuth Login")
|
||||
self._entry_key.set_placeholder_text("Auto-filled by OAuth")
|
||||
else:
|
||||
self._entry_key.set_placeholder_text("")
|
||||
@@ -3130,7 +3179,9 @@ class EditEndpointDialog(Gtk.Dialog):
|
||||
preset_name = self._combo_preset.get_active_text() 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"):
|
||||
@@ -3406,6 +3457,117 @@ class EditEndpointDialog(Gtk.Dialog):
|
||||
dlg.connect("response", lambda d, r: d.destroy())
|
||||
dlg.run()
|
||||
|
||||
def _codebuff_oauth_flow(self):
|
||||
dlg = Gtk.Dialog(title="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)
|
||||
|
||||
self._oauth_status = Gtk.Label(label="Requesting login URL…", xalign=0)
|
||||
self._oauth_status.set_line_wrap(True)
|
||||
self._oauth_status.set_max_width_chars(60)
|
||||
area.pack_start(self._oauth_status, 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)
|
||||
|
||||
self._fb_oauth_result = {"success": False, "user": None, "error": None}
|
||||
|
||||
def _codebuff_auth_thread():
|
||||
try:
|
||||
fingerprint_id = str(uuid.uuid4())
|
||||
auth_url = "https://codebuff.com/api/auth/cli/code"
|
||||
body = json.dumps({"fingerprintId": fingerprint_id}).encode()
|
||||
req = urllib.request.Request(auth_url, data=body,
|
||||
headers={"Content-Type": "application/json", "User-Agent": "codex-launcher/3.8.3"})
|
||||
resp = urllib.request.urlopen(req, timeout=30)
|
||||
data = json.loads(resp.read())
|
||||
login_url = data.get("loginUrl", "") or data.get("login_url", "")
|
||||
fingerprint_hash = data.get("fingerprintHash", "") or data.get("fingerprint_hash", "")
|
||||
expires_at = data.get("expiresAt", 0) or data.get("expires_at", 0)
|
||||
if not login_url:
|
||||
self._fb_oauth_result["error"] = "Server returned no login URL"
|
||||
GLib.idle_add(self._codebuff_oauth_done, dlg, spinner)
|
||||
return
|
||||
|
||||
def _set_link():
|
||||
self._oauth_status.set_text("Open this URL in your browser to log in:")
|
||||
link_lbl.set_markup(f'<a href="{login_url}">{login_url}</a>')
|
||||
link_lbl.set_visible(True)
|
||||
GLib.idle_add(_set_link)
|
||||
|
||||
webbrowser.open(login_url)
|
||||
|
||||
poll_url = f"https://codebuff.com/api/auth/cli/status?fingerprintId={urllib.parse.quote(fingerprint_id)}&fingerprintHash={urllib.parse.quote(fingerprint_hash)}&expiresAt={expires_at}"
|
||||
deadline = time.time() + 300
|
||||
while time.time() < deadline:
|
||||
time.sleep(2)
|
||||
try:
|
||||
poll_req = urllib.request.Request(poll_url,
|
||||
headers={"User-Agent": "codex-launcher/3.8.3"})
|
||||
poll_resp = urllib.request.urlopen(poll_req, timeout=10)
|
||||
poll_data = json.loads(poll_resp.read())
|
||||
user = poll_data.get("user")
|
||||
if user and user.get("authToken"):
|
||||
self._fb_oauth_result["success"] = True
|
||||
self._fb_oauth_result["user"] = user
|
||||
GLib.idle_add(self._codebuff_oauth_done, dlg, spinner)
|
||||
return
|
||||
except urllib.error.HTTPError:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
self._fb_oauth_result["error"] = "Login timed out after 5 minutes."
|
||||
GLib.idle_add(self._codebuff_oauth_done, dlg, spinner)
|
||||
except Exception as e:
|
||||
self._fb_oauth_result["error"] = str(e)[:200]
|
||||
GLib.idle_add(self._codebuff_oauth_done, dlg, spinner)
|
||||
|
||||
threading.Thread(target=_codebuff_auth_thread, daemon=True).start()
|
||||
dlg.connect("response", lambda d, r: d.destroy())
|
||||
dlg.run()
|
||||
|
||||
def _codebuff_oauth_done(self, dlg, spinner):
|
||||
spinner.stop()
|
||||
if self._fb_oauth_result["success"] and self._fb_oauth_result["user"]:
|
||||
user = self._fb_oauth_result["user"]
|
||||
creds_path = os.path.expanduser("~/.config/manicode/credentials.json")
|
||||
os.makedirs(os.path.dirname(creds_path), exist_ok=True)
|
||||
creds = {"default": {
|
||||
"id": user.get("id", ""),
|
||||
"name": user.get("name", ""),
|
||||
"email": user.get("email", ""),
|
||||
"authToken": user.get("authToken", ""),
|
||||
"fingerprintId": user.get("fingerprintId", ""),
|
||||
"fingerprintHash": user.get("fingerprintHash", ""),
|
||||
}}
|
||||
with open(creds_path, "w") as f:
|
||||
json.dump(creds, f, indent=2)
|
||||
os.chmod(creds_path, 0o600)
|
||||
self._entry_key.set_text(user.get("authToken", ""))
|
||||
self._oauth_status.set_markup('<span foreground="#27ae60" weight="bold">Authorization successful! Credentials saved.</span>')
|
||||
dlg.set_title("Codebuff Login – Success")
|
||||
GLib.timeout_add(1500, lambda: dlg.response(Gtk.ResponseType.OK))
|
||||
else:
|
||||
self._oauth_status.set_markup(f'<span foreground="#e74c3c">{self._fb_oauth_result["error"] or "Login failed."}</span>')
|
||||
GLib.timeout_add(3000, lambda: dlg.response(Gtk.ResponseType.CANCEL))
|
||||
|
||||
def _oauth_success(self, dlg, access_token, spinner):
|
||||
spinner.stop()
|
||||
self._entry_key.set_text(access_token)
|
||||
|
||||
Reference in New Issue
Block a user