diff --git a/CHANGELOG.md b/CHANGELOG.md index 35e930b..6e019b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/codex-launcher_2.0.0_all.deb b/codex-launcher_2.0.0_all.deb deleted file mode 100644 index 72613b1..0000000 Binary files a/codex-launcher_2.0.0_all.deb and /dev/null differ diff --git a/codex-launcher_2.0.1_all.deb b/codex-launcher_2.0.1_all.deb new file mode 100644 index 0000000..141678b Binary files /dev/null and b/codex-launcher_2.0.1_all.deb differ diff --git a/src/codex-launcher-gui b/src/codex-launcher-gui index a7bf22e..6a65119 100755 --- a/src/codex-launcher-gui +++ b/src/codex-launcher-gui @@ -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="Codex Launcher v2.0.0") + lbl = Gtk.Label(label="Codex Launcher v2.0.1") 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"✔ Codex CLI {cli_ver} ({cli_path})") + cli_lbl.set_use_markup(True) + ver_box.pack_start(cli_lbl, False, False, 0) + else: + cli_lbl = Gtk.Label() + cli_lbl.set_markup("✘ Codex CLI — not found") + 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"✔ Codex Desktop ({self._desktop_info})") + desk_lbl.set_use_markup(True) + ver_box.pack_start(desk_lbl, False, False, 0) + else: + desk_lbl = Gtk.Label() + desk_lbl.set_markup("✘ Codex Desktop — not found") + 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):