From 660b320a671ffb086c220697f9e6ef1d06e7fadd Mon Sep 17 00:00:00 2001 From: timkjr Date: Sat, 1 Aug 2026 22:01:15 -0500 Subject: [PATCH] fix(run-mode): gate dropdown entries on CLI availability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the welcome-screen gating (#200): the run-mode dropdown (gear menu next to Run) had the same problem — Claude/Opencode/Codex/ Gemini entries were always shown regardless of whether the CLI is actually installed, so picking one could spawn a session that immediately errors out. - Add _refreshRunModeAvailability() (session-ui.js), called each time the dropdown opens; hides entries whose /api//status reports unavailable. - Shell is intentionally never gated (no external CLI dependency). Depends on isClaudeAvailable()/GET /api/claude/status, which don't exist on upstream/master yet — duplicated here from #200 so this PR is self-contained and independently mergeable. Once #200 lands this branch should be rebased onto master, which will collapse the duplicate cleanly. --- src/utils/claude-cli-resolver.ts | 9 +++++++++ src/web/public/session-ui.js | 27 +++++++++++++++++++++++++++ src/web/routes/system-routes.ts | 12 +++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/utils/claude-cli-resolver.ts b/src/utils/claude-cli-resolver.ts index b92922018..6d735fa5e 100644 --- a/src/utils/claude-cli-resolver.ts +++ b/src/utils/claude-cli-resolver.ts @@ -26,6 +26,15 @@ const CLAUDE_SEARCH_DIRS = [ /** Cached directory containing the claude binary (empty string = searched but not found) */ let _claudeDir: string | null = null; +/** + * Returns true if the Claude CLI binary can be located (via `which` or one of + * the common install directories). Mirrors `isGeminiAvailable`/`isOpenCodeAvailable`/ + * `isCodexAvailable` in the sibling resolvers. + */ +export function isClaudeAvailable(): boolean { + return findClaudeDir() !== null; +} + /** * Finds the directory containing the `claude` binary. * Checks `which claude` first, then falls back to common install locations. diff --git a/src/web/public/session-ui.js b/src/web/public/session-ui.js index 0c387b864..23f0bf696 100644 --- a/src/web/public/session-ui.js +++ b/src/web/public/session-ui.js @@ -441,6 +441,7 @@ Object.assign(CodemanApp.prototype, { // Load history sessions when menu opens if (menu.classList.contains('active')) { this._loadRunModeHistory(); + this._refreshRunModeAvailability(); const close = (ev) => { if (!menu.contains(ev.target)) { menu.classList.remove('active'); @@ -451,6 +452,32 @@ Object.assign(CodemanApp.prototype, { } }, + /** + * Hides run-mode dropdown entries for CLIs that aren't installed, so + * picking one doesn't spawn a session that immediately errors out. + * Shell has no external CLI dependency and is never gated. + */ + async _refreshRunModeAvailability() { + const checks = [ + ['claude', '/api/claude/status'], + ['opencode', '/api/opencode/status'], + ['codex', '/api/codex/status'], + ['gemini', '/api/gemini/status'], + ]; + await Promise.all(checks.map(async ([mode, url]) => { + const btn = document.querySelector(`.run-mode-option[data-mode="${mode}"]`); + if (!btn) return; + try { + const res = await fetch(url); + const env = await res.json(); + const status = env?.success === true ? env.data : env; + btn.style.display = status?.available ? 'flex' : 'none'; + } catch { + btn.style.display = 'none'; + } + })); + }, + async _loadRunModeHistory() { const container = document.getElementById('runModeHistory'); if (!container) return; diff --git a/src/web/routes/system-routes.ts b/src/web/routes/system-routes.ts index 4715ac913..64e43c762 100644 --- a/src/web/routes/system-routes.ts +++ b/src/web/routes/system-routes.ts @@ -374,9 +374,19 @@ export function registerSystemRoutes( }); // ═══════════════════════════════════════════════════════════════ - // CLI Integrations (OpenCode, Codex, Gemini, Antigravity) + // CLI Integrations (Claude, OpenCode, Codex, Gemini, Antigravity) // ═══════════════════════════════════════════════════════════════ + // ========== Claude ========== + + app.get('/api/claude/status', async () => { + const { isClaudeAvailable, findClaudeDir } = await import('../../utils/claude-cli-resolver.js'); + return { + available: isClaudeAvailable(), + path: findClaudeDir(), + }; + }); + // ========== OpenCode ========== app.get('/api/opencode/status', async () => {