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