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:
admin
2026-05-24 19:30:10 +04:00
Unverified
parent f9a83e4862
commit df9e9d1fa6
5 changed files with 282 additions and 54 deletions

View File

@@ -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)