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 () => {