v2.0.1: Add Codex CLI/Desktop installation verifier

- Detects codex CLI (via 'which codex' + --version) and Desktop (via /opt/codex-desktop/start.sh)
- Shows green ✔ or yellow ✘ status bar on main page
- 'Install' button opens guide dialog with install commands
- Desktop/CLI launch buttons disabled when corresponding tool is missing
- Dependency status logged on startup
- _set_busy respects missing-state (won't re-enable disabled buttons)
- Rebuilt .deb as v2.0.1
This commit is contained in:
admin
2026-05-19 15:32:44 +04:00
Unverified
parent 50ba552635
commit ac24fe6372
4 changed files with 136 additions and 8 deletions

View File

@@ -1,5 +1,14 @@
# Changelog
## v2.0.1 (2026-05-19)
- Added Codex CLI/Desktop installation verifier to main page
- Green check (✔) when detected, yellow cross (✘) when missing
- "Install" button next to missing tools opens install guide dialog
- Desktop/CLI launch buttons disabled with tooltip when tool is missing
- Dependency status logged on startup
- Buttons respect missing-state after busy/unbusy cycles
## v2.0.0 (2026-05-19)
- Initial release: multi-provider Codex Launcher

Binary file not shown.

Binary file not shown.

View File

@@ -24,6 +24,11 @@ model_catalog_json = ""
"""
CHANGELOG = [
("2.0.1", "2026-05-19", [
"Added Codex CLI/Desktop installation verifier to main page",
"Disables Desktop/CLI launch buttons when corresponding tool is missing",
"Shows install instructions in status area on startup",
]),
("2.0.0", "2026-05-19", [
"Initial release: multi-provider Codex Launcher",
"Translation proxy: Responses API to Chat Completions + Anthropic Messages",
@@ -429,6 +434,22 @@ def _last_log_lines(n=15):
except Exception:
return "(no log file)"
def _detect_codex_cli():
try:
path = shutil.which("codex")
if not path:
return None
out = subprocess.run(["codex", "--version"], capture_output=True, text=True, timeout=5)
ver = (out.stdout or "").strip() or (out.stderr or "").strip() or "unknown"
return (path, ver)
except Exception:
return None
def _detect_codex_desktop():
if START_SH.exists():
return str(START_SH)
return None
# ═══════════════════════════════════════════════════════════════════
# Main window
# ═══════════════════════════════════════════════════════════════════
@@ -448,7 +469,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 v2.0.0</b>")
lbl = Gtk.Label(label="<b>Codex Launcher v2.0.1</b>")
lbl.set_use_markup(True)
hdr.pack_start(lbl, False, False, 0)
changelog_btn = Gtk.Button(label="Changelog")
@@ -458,6 +479,49 @@ class LauncherWin(Gtk.Window):
mgr_btn.connect("clicked", lambda b: self._open_mgr())
hdr.pack_end(mgr_btn, False, False, 0)
# verification status bar
self._cli_info = _detect_codex_cli()
self._desktop_info = _detect_codex_desktop()
ver_box = Gtk.Box(spacing=12)
vbox.pack_start(ver_box, False, False, 0)
if self._cli_info:
cli_path, cli_ver = self._cli_info
cli_lbl = Gtk.Label()
cli_lbl.set_markup(f"<span foreground='#2ea043'>✔ Codex CLI</span> <small>{cli_ver} ({cli_path})</small>")
cli_lbl.set_use_markup(True)
ver_box.pack_start(cli_lbl, False, False, 0)
else:
cli_lbl = Gtk.Label()
cli_lbl.set_markup("<span foreground='#d29922'>✘ Codex CLI — not found</span>")
cli_lbl.set_use_markup(True)
ver_box.pack_start(cli_lbl, False, False, 0)
cli_install_btn = Gtk.Button(label="Install")
cli_install_btn.connect("clicked", lambda b: self._show_install_guide("cli"))
ver_box.pack_start(cli_install_btn, False, False, 0)
ver_box.pack_start(Gtk.Label(label=" "), False, False, 0)
if self._desktop_info:
desk_lbl = Gtk.Label()
desk_lbl.set_markup(f"<span foreground='#2ea043'>✔ Codex Desktop</span> <small>({self._desktop_info})</small>")
desk_lbl.set_use_markup(True)
ver_box.pack_start(desk_lbl, False, False, 0)
else:
desk_lbl = Gtk.Label()
desk_lbl.set_markup("<span foreground='#d29922'>✘ Codex Desktop — not found</span>")
desk_lbl.set_use_markup(True)
ver_box.pack_start(desk_lbl, False, False, 0)
desk_install_btn = Gtk.Button(label="Install")
desk_install_btn.connect("clicked", lambda b: self._show_install_guide("desktop"))
ver_box.pack_start(desk_install_btn, False, False, 0)
self._missing = []
if not self._cli_info:
self._missing.append("cli")
if not self._desktop_info:
self._missing.append("desktop")
ops_box = Gtk.Box(spacing=8)
vbox.pack_start(ops_box, False, False, 0)
self._refresh_all_btn = Gtk.Button(label="Refresh Models")
@@ -488,18 +552,30 @@ class LauncherWin(Gtk.Window):
vbox.pack_start(btn_box, False, False, 8)
self._btn_desktop = Gtk.Button(label="Launch Desktop")
self._btn_desktop.connect("clicked", lambda b: self._launch("desktop"))
if "desktop" in self._missing:
self._btn_desktop.set_tooltip_text("Codex Desktop is not installed")
self._btn_desktop.set_sensitive(False)
btn_box.pack_start(self._btn_desktop, True, True, 0)
self._btn_cli = Gtk.Button(label="Launch CLI")
self._btn_cli.connect("clicked", lambda b: self._launch("cli"))
if "cli" in self._missing:
self._btn_cli.set_tooltip_text("Codex CLI is not installed")
self._btn_cli.set_sensitive(False)
btn_box.pack_start(self._btn_cli, True, True, 0)
btn_box2 = Gtk.Box(spacing=8, homogeneous=True)
vbox.pack_start(btn_box2, False, False, 0)
self._btn_codex_desktop = Gtk.Button(label="Codex Default (Desktop)")
self._btn_codex_desktop.connect("clicked", lambda b: self._launch_codex_default("desktop"))
if "desktop" in self._missing:
self._btn_codex_desktop.set_tooltip_text("Codex Desktop is not installed")
self._btn_codex_desktop.set_sensitive(False)
btn_box2.pack_start(self._btn_codex_desktop, True, True, 0)
self._btn_codex_cli = Gtk.Button(label="Codex Default (CLI)")
self._btn_codex_cli.connect("clicked", lambda b: self._launch_codex_default("cli"))
if "cli" in self._missing:
self._btn_codex_cli.set_tooltip_text("Codex CLI is not installed")
self._btn_codex_cli.set_sensitive(False)
btn_box2.pack_start(self._btn_codex_cli, True, True, 0)
# status
@@ -529,6 +605,7 @@ class LauncherWin(Gtk.Window):
self.show_all()
self._rebuild_combo()
self._log_dependency_status()
# ── helpers ──────────────────────────────────────────────────
@@ -542,14 +619,31 @@ class LauncherWin(Gtk.Window):
self._tv.scroll_to_mark(m, 0.0, True, 0.0, 0.5)
self._buf.delete_mark(m)
def _log_dependency_status(self):
if self._cli_info:
_, ver = self._cli_info
self.log(f"✔ Codex CLI detected ({ver})")
else:
self.log("✘ Codex CLI NOT found — CLI launch disabled. Click 'Install' above.")
if self._desktop_info:
self.log(f"✔ Codex Desktop detected ({self._desktop_info})")
else:
self.log("✘ Codex Desktop NOT found — Desktop launch disabled. Click 'Install' above.")
if self._missing:
self.log("⚠ Install missing tools before using the launcher.")
else:
self.log("All dependencies OK.")
def _set_busy(self, busy):
GLib.idle_add(lambda: (
self._btn_desktop.set_sensitive(not busy),
self._btn_cli.set_sensitive(not busy),
self._btn_codex_desktop.set_sensitive(not busy),
self._btn_codex_cli.set_sensitive(not busy),
self._kill_btn.set_sensitive(busy),
))
def _update():
has_cli = "cli" not in self._missing
has_desk = "desktop" not in self._missing
self._btn_desktop.set_sensitive(not busy and has_desk)
self._btn_cli.set_sensitive(not busy and has_cli)
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)
GLib.idle_add(_update)
def _rebuild_combo(self):
self._endpoints_data = load_endpoints()
@@ -746,6 +840,31 @@ class LauncherWin(Gtk.Window):
d.run()
d.destroy()
def _show_install_guide(self, which):
if which == "cli":
title = "Install Codex CLI"
guide = (
"Codex CLI is required to use CLI launch features.\n\n"
"Install with npm:\n"
" npm install -g @openai/codex\n\n"
"Or download from:\n"
" https://github.com/openai/codex\n\n"
"After installing, restart the launcher."
)
else:
title = "Install Codex Desktop"
guide = (
"Codex Desktop is required to use Desktop launch features.\n\n"
"Expected location: /opt/codex-desktop/start.sh\n\n"
"Download from:\n"
" https://codex.desktop.openai.com\n\n"
"After installing, restart the launcher."
)
d = Gtk.MessageDialog(self, 0, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, guide)
d.set_title(title)
d.run()
d.destroy()
# ── launch ───────────────────────────────────────────────────
def _launch(self, target):