Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/utils/claude-cli-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 27 additions & 0 deletions src/web/public/session-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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;
Expand Down
12 changes: 11 additions & 1 deletion src/web/routes/system-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down