8000 Display agent availability during onboarding (vibe-kanban) (#1352) · BloopAI/vibe-kanban@fd5ef91 · GitHub
[go: up one dir, main page]

Skip to content

Commit fd5ef91

Browse files
authored
Display agent availability during onboarding (vibe-kanban) (#1352)
* Perfect! All the implementation is complete. Let me summarize what was done: I've successfully implemented agent availability checking during onboarding, mirroring the existing IDE availability functionality. Here's what was added: 1. **New API endpoint** in `crates/server/src/routes/config.rs`: - Added `CheckAgentAvailabilityQuery` struct with `executor: BaseCodingAgent` - Added `CheckAgentAvailabilityResponse` struct with `available: bool` - Added `check_agent_availability()` handler that uses the existing `check_availability()` method from the executors crate - Registered route: `/api/agents/check-availability` 2. **Type generation** in `crates/server/src/bin/generate_types.rs`: - Added the new types to the TypeScript generation list 3. **API client** in `frontend/src/lib/api.ts`: - Added `checkAgentAvailability()` method to `configApi` - Imported necessary types 4. **Custom hook** in `frontend/src/hooks/useAgentAvailability.ts`: - Created hook that checks agent availability asynchronously - Returns states: `'checking' | 'available' | 'unavailable' | null` 5. **UI component** in `frontend/src/components/AgentAvailabilityIndicator.tsx`: - Visual indicator showing spinner (checking), checkmark (available), or warning icon (unavailable) - Matches the style of `EditorAvailabilityIndicator` 6. **Onboarding dialog** in `frontend/src/components/dialogs/global/OnboardingDialog.tsx`: - Added agent availability check using the hook - Displays the availability indicator below the agent selection dropdown - The backend leverages existing `check_availability()` method that checks for MCP config files - Frontend pattern exactly mirrors the IDE availability checking for consistency - All TypeScript types are auto-generated from Rust - Both backend (Rust) and frontend (TypeScript) checks pass successfully The implementation is complete and ready for use! Cleanup script changes for task attempt 60b68945-b59c-46e1-895d-540a910f2904 Great! TypeScript compilation passes. The semantic color classes have been applied successfully. The component now uses: - `text-success` for available agents (instead of `text-green-500`/`text-green-600`) - `text-warning` for unavailable agents (instead of `text-orange-500`/`text-orange-600`) This matches the semantic color conventions used throughout the codebase and will properly adapt to theme changes. Excellent! I've implemented file modification time checking for Claude Code's credential file. Here's what the enhanced availability check does: The `check_availability()` method for Claude Code now validates: 1. **File exists**: `~/.claude.json` must exist 2. **File is fresh**: Credentials file must have been modified within the last **60 days** - If the file hasn't been touched in 60 days, it's considered stale and the agent is marked as unavailable - This handles the case where credentials might have expired but the file still exists - **OAuth refresh updates the file**: When credentials are refreshed (e.g., access token renewed), the file gets rewritten with new tokens, updating the modification time - **Simple and fast**: Just a filesystem metadata check, no JSON parsing or API calls needed - **Conservative threshold**: 60 days is reasonable - most OAuth refresh tokens are valid for 30-90 days, so if nothing has touched the file in 60 days, it's likely expired - **False negatives possible**: If someone logged in 61 days ago and never used Claude Code since, it might still work (refresh token could be valid for 90 days), but we'll show it as unavailable - **False positives possible**: File could be recently modified but tokens could be manually revoked server-side - **Not foolproof**: The only way to be 100% certain is to make an authenticated API call But as a heuristic for showing availability during onboarding, this is a solid approach that balances accuracy with performance. Cleanup script changes for task attempt 60b68945-b59c-46e1-895d-540a910f2904 Perfect! All checks pass. Let me summarize what was implemented: - Enhanced `check_availability()` to check `~/.claude.json` modification time - Marks agent as unavailable if credential file is older than 60 days - Uses `std::fs::metadata()` and `modified().elapsed()` to compute age - `CheckAgentAvailabilityResponse` now includes `credential_last_modified: Option<i64>` - Calls new trait method `get_credential_last_modified()` to get Unix timestamp - Returns `None` if file doesn't exist or metadata can't be read - Added `get_credential_last_modified()` to `StandardCodingAgentExecutor` trait - Gets file metadata and converts modification time to Unix timestamp - Works for all agents automatically (uses their `default_mcp_config_path()`) - On mount, checks availability of ALL agents in parallel - Filters to only available agents with credentials - Sorts by most recent `credential_last_modified` timestamp - Auto-selects the agent with freshest credentials - Only runs if user hasn't already configured an agent 1. User opens onboarding dialog for the first time 2. Frontend checks all agents in parallel (CLAUDE_CODE, CODEX, CURSOR_AGENT, etc.) 3. Each returns `{ available: bool, credential_last_modified: timestamp }` 4. Frontend finds agent with most recent credential file modification 5. Auto-selects that agent (likely the one they used most recently) 6. User can still manually change if they want This provides a smart default while still allowing user choice! Perfect! All checks pass. Let me show you the final summary: I've successfully implemented comprehensive agent availability checking with automatic selection of the most recently used agent. Here's what was added: **`AvailabilityInfo` struct** (`crates/executors/src/executors/mod.rs:167-206`): - Structured type with fields: `mcp_config_found`, `auth_config_found`, `auth_last_edited` - `is_available()` method that determines availability from the signals - Logic: Available if MCP config exists OR auth config exists and is < 60 days old **Trait method** (`crates/executors/src/executors/mod.rs:229-241`): - Added `get_availability_info() -> AvailabilityInfo` to `StandardCodingAgentExecutor` - Default implementation checks only MCP config - Updated `check_availability()` to use `get_availability_info().is_available()` **Claude Code** (`crates/executors/src/executors/claude.rs:200-224`): - Auth credentials: `~/.claude.json` - Checks file existence and modification time - Returns AvailabilityInfo with auth signals **Codex** (`crates/executors/src/executors/codex.rs:168-195`): - MCP config: `~/.codex/config.toml` - Auth credentials: `~/.codex/auth.json` - Checks both MCP and auth configs **Gemini** (`crates/executors/src/executors/gemini.rs:96-123`): - MCP config: `~/.gemini/settings.json` - Auth credentials: `~/.gemini/oauth_creds.json` - Checks both MCP and auth configs **Backend API** (`crates/server/src/routes/config.rs:479-498`): - Returns `credential_last_modified` timestamp from AvailabilityInfo - Uses `info.is_available()` for the available field **Frontend auto-selection** (`frontend/src/components/dialogs/global/OnboardingDialog.tsx:64-112`): - On mount, checks all agents in parallel - Filters to available agents with credentials - **Sorts by most recent `credential_last_modified`** - Auto-selects the agent used most recently This provides intelligent defaults while respecting user choice! Perfect! All checks pass. Here's what I implemented: **Enhanced API Response** (`crates/server/src/routes/config.rs:472-481`): ```rust pub struct CheckAgentAvailabilityResponse { available: bool, mcp_config_found: bool, auth_config_found: bool, credential_last_modified: Option<i64>, } ``` Now returns all the availability signals from `AvailabilityInfo`. **OnboardingDialog** (`frontend/src/components/dialogs/global/OnboardingDialog.tsx`): 1. **State for availability map** (lines 57-59): - Stores full availability info for all agents - Used for intelligent sorting 2. **Priority-based sorting** (lines 187-218): - **Tier 1**: Agents with auth config - sorted by most recent first - **Tier 2**: Agents with MCP config only - alphabetical within tier - **Tier 3**: Other agents - alphabetical within tier - **Fallback**: Alphabetical if no availability info loaded yet ```typescript // Tier 1: Both have auth - sort by recency if (infoA.auth_config_found && infoB.auth_config_found) { return timeB - timeA; // Most recent first } // Tier 2: Auth trumps MCP-only if (infoA.auth_config_found) return -1; if (infoB.auth_config_found) return 1; // Tier 3: Both have MCP only - alphabetical if (infoA.mcp_config_found && infoB.mcp_config_found) { return a.localeCompare(b); } ``` The dropdown will now show agents in this order: 1. **Most recently authenticated agents first** (e.g., Claude Code used yesterday) 2. **Other authenticated agents** (e.g., Gemini used last week) 3. **Agents with only MCP config** (alphabetical) 4. **Agents with no config** (alphabetical) This ensures users see their most relevant agents at the top of the list! Cleanup script changes for task attempt 60b68945-b59c-46e1-895d-540a910f2904 Perfect! All checks pass. Here's the final implementation summary: **Removed `auth_config_found` field** - simplified to just use `auth_last_edited.is_some()`: **AvailabilityInfo** (`crates/executors/src/executors/mod.rs:167-201`): ```rust pub struct AvailabilityInfo { pub mcp_config_found: bool, pub auth_last_edited: Option<i64>, // If Some(_), auth exists } ``` **Implementations:** - **Claude Code** (`claude.rs:200-212`): `~/.claude.json` → `auth_last_edited` - **Codex** (`codex.rs:168-185`): MCP at `~/.codex/config.toml`, auth at `~/.codex/auth.json` - **Gemini** (`gemini.rs:96-113`): MCP at `~/.gemini/settings.json`, auth at `~/.gemini/oauth_creds.json` **CheckAgentAvailabilityResponse** (`config.rs:472-480`): ```rust { available: bool, mcp_config_found: bool, credential_last_modified: Option<i64>, // If Some, auth file exists } ``` **Priority-based dropdown ordering** (`OnboardingDialog.tsx:191-230`): 1. **Tier 1**: Agents with auth (`credential_last_modified !== null`) - sorted by most recent 2. **Tier 2**: Agents with MCP only - alphabetical 3. **Tier 3**: Others - alphabetical Users will see: - **Top of list**: Their most recently authenticated agents (e.g., "logged into Claude Code yesterday") - **Middle**: Agents with MCP config but no recent auth - **Bottom**: Remaining agents alphabetically The implementation is clean, simple, and provides intelligent defaults! Cleanup script changes for task attempt 60b68945-b59c-46e1-895d-540a910f2904 Perfect! All checks pass. The fix is complete: **The Problem:** - Line 76 had: `if (!profiles || config?.executor_profile) return;` - This meant availability was **never fetched** for users with existing config - `agentAvailabilityMap` stayed empty, sorting defaulted to alphabetical - Result: AMP appeared first **The Solution:** - Removed `config?.executor_profile` from the skip condition - Now availability is **always fetched** to enable sorting - Moved the `config?.executor_profile` check **inside** the function (line 116) - Auto-selection only happens for new users, but sorting works for everyone **Now the dropdown will show:** 1. **Agents with recent auth** - sorted by most recent first (e.g., Claude Code used yesterday) 2. **Agents with MCP config only** - alphabetical (e.g., Cursor with MCP setup) 3. **Other agents** - alphabetical The priority-based sorting is now fully functional! Perfect! Fixed. Both indicators now use the same semantic colors: - `text-success` for available (instead of `text-green-500`/`text-green-600`) - `text-warning` for unavailable (instead of `text-orange-500`/`text-orange-600`) The colors will now match perfectly between IDE and coding agent availability indicators. Great! TypeScript compiles successfully. The changes are complete: **AgentAvailabilityIndicator** now has: 1. **Updated text:** - "Available" → "Login Detected" - "Not Found" remains "Not Found" 2. **Tooltips on hover:** - **Login Detected**: "Recent authentication credentials found for this agent" - **Not Found**: "No credentials detected. The agent may still work if configured differently." 3. **Visual improvements:** - Added `cursor-help` class to show the help cursor on hover - Wrapped with shadcn/ui `Tooltip` components This provides clearer messaging and helpful context for users! Perfect! The tooltips now have `className="z-[100]"` which should render them above the modal. The fix ensures the hover text is readable when shown within the onboarding dialog. Perfect! The tooltips now use `z-[10000]` which is higher than the modal's `z-[9999]`, so they should appear on top and be readable. Perfect! TypeScript compiles successfully. The agent availability indicator is now added to the AgentSettings page: **AgentSettings.tsx** now has: 1. **Availability check** (line 65): ```typescript const agentAvailability = useAgentAvailability(selectedExecutorType); ``` 2. **Indicator display** (line 444): ```typescript <AgentAvailabilityIndicator availability={agentAvailability} /> ``` The indicator appears below the agent dropdown selector and shows: - "Login Detected" with tooltip for agents with credentials - "Not Found" with tooltip for agents without credentials - Same styling and behavior as the onboarding dialog This provides consistent availability feedback throughout the app! Cleanup script changes for task attempt 60b68945-b59c-46e1-895d-540a910f2904 Excellent! TypeScript compiles successfully. The changes are complete: **Reverted AgentSettings.tsx:** - Removed the availability indicator from the Agent Settings page (which is for advanced profile configuration) **Added to GeneralSettings.tsx:** - **Line 43-44**: Imported `useAgentAvailability` hook and `AgentAvailabilityIndicator` component - **Line 81**: Added hook call: `const agentAvailability = useAgentAvailability(draft?.executor_profile?.executor);` - **Line 420**: Added indicator display: `<AgentAvailabilityIndicator availability={agentAvailability} />` The agent availability indicator now appears in the **General Settings** page under the "Task Execution" section, right below the agent dropdown selector - matching the pattern of how editor availability is shown in the same settings page. This is the correct location since it's where users configure their default agent, not the advanced agent configuration page. Cleanup script changes for task attempt 60b68945-b59c-46e1-895d-540a910f2904 Perfect! All checks pass. Comments removed and types regenerated successfully. The implementation is complete and clean. Fix agent availability i18n (vibe-kanban dcd1fc06) Run @scripts/check-i18n.sh until it passes. Make sure to check the script and set GITHUB_BASE_REF to main Fix agent availability i18n (vibe-kanban 77795175) Run @scripts/check-i18n.sh until it passes. Make sure to check the script and set GITHUB_BASE_REF to main Currently agents.availability.loginDetected shows in the frontend so it's not using i18n correctly, it was added in the last commit though Perfect! TypeScript compiles. Now the availability indicator shows three distinct states: 1. **"Login Detected"** - Has auth credentials (recent login) 2. **"Installation Found"** - Has MCP config but no auth (agent is installed) 3. **"Not Found"** - Neither auth nor MCP config found This gives users much clearer information about what was actually detected! Cleanup script changes for task attempt 60b68945-b59c-46e1-895d-540a910f2904 Perfect! Now the indicator shows three distinct states: 1. **"Login Detected"** (green check) - Has recent auth credentials - Tooltip: "Recent authentication credentials found for this agent" 2. **"Installation Found"** (green check) - Has MCP config but no auth - Tooltip: "Agent configuration found. You may need to log in to use it." 3. **"Not Found"** (orange warning) - Neither found - Tooltip: "No credentials detected. The agent may still work if configured differently." This gives users accurate, specific information about what was actually detected! Fix i18n agent availability (vibe-kanban 9eab4ea1) Run @scripts/check-i18n.sh until it passes. Make sure to check the script and set GITHUB_BASE_REF to main Perfect! I've fixed all the i18n keys by adding the `settings.` prefix. The translations should now work correctly and display: - "Login Detected" instead of "agents.availability.loginDetected" - "Installation Found" instead of "agents.availability.installationFound" - "Not Found" instead of "agents.availability.notFound" All the keys now match the pattern used throughout the codebase where `useTranslation('settings')` requires the full path starting with `settings.`. Cleanup script changes for task attempt 60b68945-b59c-46e1-895d-540a910f2904 * Perfect! All checks pass. The refactoring to use `AvailabilityInfo` directly is complete: **Removed redundancy:** - Deleted `CheckAgentAvailabilityResponse` struct entirely - API now returns `AvailabilityInfo` directly - Removed `available` field (was computed/redundant) **Backend now returns:** ```rust AvailabilityInfo { mcp_config_found: bool, auth_last_edited: Option<i64>, } ``` **Frontend interprets it as:** - `auth_last_edited !== null` → **"Login Detected"** (has recent auth credentials) - `auth_last_edited === null && mcp_config_found === true` → **"Installation Found"** (has MCP config only) - `auth_last_edited === null && mcp_config_found === false` → **"Not Found"** (nothing detected) **Benefits:** - Single source of truth for availability data structure - No duplication between `AvailabilityInfo` and response type - Frontend gets raw signals and can interpret them appropriately - Cleaner, simpler codebase The implementation now correctly distinguishes between login credentials and installation/configuration files! Cleanup script changes for task attempt 60b68945-b59c-46e1-895d-540a910f2904 Perfect! All checks pass. The `check_availability` method has been successfully removed: **Removed `check_availability()` method:** - Updated `profile.rs:419` to use `get_availability_info().is_available()` instead - Removed `check_availability()` from trait definition - Updated Cursor's custom override to use `get_availability_info()` instead - Removed unused import from cursor.rs **Now there's only one method:** - `get_availability_info()` returns the raw signals - Callers use `.is_available()` if they need a boolean Cleaner API surface with no redundant methods! * Perfect! The simplification is complete. Let me show you what was removed: ## Simplified Implementation **Removed from OnboardingDialog (~90 lines):** - ❌ `agentAvailabilityMap` state (9 lines) - ❌ `useEffect` with parallel API calls (66 lines) - ❌ Complex 3-tier sorting logic (41 lines) - ❌ Auto-selection based on recent auth - ❌ Unused imports (`useEffect`, `configApi`) **What remains:** - ✅ Simple alphabetical sort: `.sort()` - ✅ Backend auto-selects first available agent (via `get_recommended_executor_profile`) - ✅ Live indicator when user selects agent (via `useAgentAvailability` hook) - ✅ Indicator in GeneralSettings **The feature now:** 1. Dropdown shows agents alphabetically (AMP, CLAUDE_CODE, CODEX, ...) 2. Pre-selected agent is whatever backend chose (first available from HashMap) 3. User selects an agent → indicator shows "Login Detected" / "Installation Found" / "Not Found" 4. Same in GeneralSettings when changing default agent **Net reduction: ~90 lines removed from OnboardingDialog, much simpler!** * Perfect! All checks pass. The implementation is complete: ## Summary of Final Simplified Architecture **Backend (profile.rs):** - `get_recommended_executor_profile()` now checks ALL available agents - Sorts by most recent `auth_last_edited` timestamp - Returns agent with freshest credentials (e.g., logged in yesterday) - Fallback to agents with MCP config if no auth found **Frontend (OnboardingDialog):** - ✅ Shows alphabetically sorted dropdown - ✅ Pre-selects whatever backend recommended (agent with most recent credentials) - ✅ Shows live indicator when selected: "Login Detected" / "Installation Found" / "Not Found" - ❌ No parallel API calls on mount (removed ~90 lines) - ❌ No complex sorting logic (removed) - ❌ No auto-selection in frontend (backend handles it) **Net result:** - Smart auto-selection happens in backend (right place) - Frontend is much simpler (just shows indicator on selection) - Achieves the same UX goal (picks most recently used agent) - ~90 lines removed from OnboardingDialog Much cleaner architecture! * Perfect! All checks pass. The 60-day cutoff has been removed: ## Summary of Changes **Simplified `is_available()` logic** (`crates/executors/src/executors/mod.rs:175-177`): ```rust pub fn is_available(&self) -> bool { self.mcp_config_found || self.auth_last_edited.is_some() } ``` **Before:** - Auth file must exist AND be less than 60 days old - Rejected old auth files even if tokens still valid **After:** - Auth file exists → available ✅ - MCP config exists → available ✅ - Simple and straightforward **Benefits:** - ✅ Removed 13 lines of arbitrary staleness checking - ✅ No false negatives (61-day-old credentials shown as "Login Detected") - ✅ Backend sorting still prioritizes recent auth (for smart auto-selection) - ✅ If tokens are actually expired, user finds out when they try to use the agent (which is fine) The implementation is now much cleaner - file existence is all we check, and recency is only used for **prioritization** in the backend auto-selection, not for determining availability. * Review agent availability (vibe-kanban a75cb3d4) Review the last 4 commits * Move agent install info out of tooltip (vibe-kanban 481d6ed6) When agents are not installed users can get more info by hovering and looking at the tooltip, move this out of there and always display it. * Refactor auth file path (vibe-kanban e691ad16) In crates/executors/src/executors/mod.rs add a method similar to mcp config for auth config that executors can check in the availability check * Refactor auth file path (vibe-kanban e691ad16) In crates/executors/src/executors/mod.rs add a method similar to mcp config for auth config that executors can check in the availability check * Refactor availability info (vibe-kanban 8e24bec4) Check crates/executors/src/executors/mod.rs and refactor availability info. The frontend currently may not use the timestamp and defines its own state type. we should return the state from the backend instead * Perfect! Now all locale files are updated with the user-centric language: ## Summary of i18n Updates **Changed labels to user-centric language:** - "Login Detected" → **"Recent Usage Detected"** - "Installation Found" → **"Previous Usage Detected"** **Updated notFound tooltip:** - "No credentials detected. The agent may still work if configured differently." - → **"No previous usage detected. Agent may require installation and/or login."** **Kept technical tooltips for detected states:** - "Recent authentication credentials found for this agent" (still accurate) - "Agent configuration found. You may need to log in to use it." (still helpful) This is much clearer for users - focuses on what they care about (did I use this before?) rather than technical implementation details (credentials, MCP configs, auth files). * lint * Perfect! All the imports are cleaned up. Now all executor implementations use the shorter `AvailabilityInfo::LoginDetected` instead of the verbose `crate::executors::AvailabilityInfo::LoginDetected`. Cleanup script changes for task attempt 60b68945-b59c-46e1-895d-540a910f2904 Perfect! All the imports are now cleaned up. All executor implementations use the shorter `AvailabilityInfo::LoginDetected` instead of the verbose `crate::executors::AvailabilityInfo::LoginDetected`. Cleanup script changes for task attempt 60b68945-b59c-46e1-895d-540a910f2904
1 parent bd93f14 commit fd5ef91

File tree

22 files changed

+453
-33
lines changed

22 files changed

+453
-33
lines changed

crates/executors/src/executors/claude.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use crate::{
2222
approvals::ExecutorApprovalService,
2323
command::{CmdOverrides, CommandBuilder, CommandParts, apply_overrides},
2424
executors::{
25-
AppendPrompt, ExecutorError, SpawnedChild, StandardCodingAgentExecutor,
25+
AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor,
2626
codex::client::LogWriter,
2727
},
2828
logs::{
@@ -193,6 +193,23 @@ impl StandardCodingAgentExecutor for ClaudeCode {
193193
fn default_mcp_config_path(&self) -> Option<std::path::PathBuf> {
194194
dirs::home_dir().map(|home| home.join(".claude.json"))
195195
}
196+
197+
fn get_availability_info(&self) -> AvailabilityInfo {
198+
let auth_file_path = dirs::home_dir().map(|home| home.join(".claude.json"));
199+
200+
if let Some(path) = auth_file_path
201+
&& let Some(timestamp) = std::fs::metadata(&path)
202+
.ok()
203+
.and_then(|m| m.modified().ok())
204+
.and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok())
205+
.map(|d| d.as_secs() as i64)
206+
{
207+
return AvailabilityInfo::LoginDetected {
208+
last_auth_timestamp: timestamp,
209+
};
210+
}
211+
AvailabilityInfo::NotFound
212+
}
196213
}
197214

198215
impl ClaudeCode {

crates/executors/src/executors/codex.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use crate::{
3333
approvals::ExecutorApprovalService,
3434
command::{CmdOverrides, CommandBuilder, CommandParts, apply_overrides},
3535
executors::{
36-
AppendPrompt, ExecutorError, SpawnedChild, StandardCodingAgentExecutor,
36+
AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor,
3737
codex::{jsonrpc::ExitSignalSender, normalize_logs::Error},
3838
},
3939
stdout_dup::create_stdout_pipe_writer,
@@ -164,6 +164,34 @@ impl StandardCodingAgentExecutor for Codex {
164164
fn default_mcp_config_path(&self) -> Option<PathBuf> {
165165
dirs::home_dir().map(|home| home.join(".codex").join("config.toml"))
166166
}
167+
168+
fn get_availability_info(&self) -> AvailabilityInfo {
169+
if let Some(timestamp) = dirs::home_dir()
170+
.and_then(|home| std::fs::metadata(home.join(".codex").join("auth.json")).ok())
171+
.and_then(|m| m.modified().ok())
172+
.and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok())
173+
.map(|d| d.as_secs() as i64)
174+
{
175+
return AvailabilityInfo::LoginDetected {
176+
last_auth_timestamp: timestamp,
177+
};
178+
}
179+
180+
let mcp_config_found = self
181+
.default_mcp_config_path()
182+
.map(|p| p.exists())
183+
.unwrap_or(false);
184+
185+
let installation_indicator_found = dirs::home_dir()
186+
.map(|home| home.join(".codex").join("version.json").exists())
187+
.unwrap_or(false);
188+
189+
if mcp_config_found || installation_indicator_found {
190+
AvailabilityInfo::InstallationFound
191+
} else {
192+
AvailabilityInfo::NotFound
193+
}
194+
}
167195
}
168196

169197
impl Codex {

crates/executors/src/executors/copilot.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ use workspace_utils::{msg_store::MsgStore, path::get_vibe_kanban_temp_dir};
2323

2424
use crate::{
2525
command::{CmdOverrides, CommandBuilder, apply_overrides},
26-
executors::{AppendPrompt, ExecutorError, SpawnedChild, StandardCodingAgentExecutor},
26+
executors::{
27+
AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor,
28+
},
2729
logs::{
2830
NormalizedEntry, NormalizedEntryType, plain_text_processor::PlainTextLogProcessor,
2931
stderr_processor::normalize_stderr_logs, utils::EntryIndexProvider,
@@ -197,6 +199,23 @@ impl StandardCodingAgentExecutor for Copilot {
197199
fn default_mcp_config_path(&self) -> Option<std::path::PathBuf> {
198200
dirs::home_dir().map(|home| home.join(".copilot").join("mcp-config.json"))
199201
}
202+
203+
fn get_availability_info(&self) -> AvailabilityInfo {
204+
let mcp_config_found = self
205+
.default_mcp_config_path()
206+
.map(|p| p.exists())
207+
.unwrap_or(false);
208+
209+
let installation_indicator_found = dirs::home_dir()
210+
.map(|home| home.join(".copilot").join("config.json").exists())
211+
.unwrap_or(false);
212+
213+
if mcp_config_found || installation_indicator_found {
214+
AvailabilityInfo::InstallationFound
215+
} else {
216+
AvailabilityInfo::NotFound
217+
}
218+
}
200219
}
201220

202221
impl Copilot {

crates/executors/src/executors/cursor.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ use workspace_utils::{
1212
diff::{concatenate_diff_hunks, create_unified_diff, extract_unified_diff_hunks},
1313
msg_store::MsgStore,
1414
path::make_path_relative,
15-
shell::resolve_executable_path,
15+
shell::resolve_executable_path_blocking,
1616
};
1717

1818
use crate::{
1919
command::{CmdOverrides, CommandBuilder, apply_overrides},
20-
executors::{AppendPrompt, ExecutorError, SpawnedChild, StandardCodingAgentExecutor},
20+
executors::{
21+
AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor,
22+
},
2123
logs::{
2224
ActionType, FileChange, NormalizedEntry, NormalizedEntryError, NormalizedEntryType,
2325
TodoItem, ToolStatus,
@@ -467,13 +469,26 @@ impl StandardCodingAgentExecutor for CursorAgent {
467469
});
468470
}
469471

470-
// MCP configuration methods
471472
fn default_mcp_config_path(&self) -> Option<std::path::PathBuf> {
472473
dirs::home_dir().map(|home| home.join(".cursor").join("mcp.json"))
473474
}
474475

475-
async fn check_availability(&self) -> bool {
476-
resolve_executable_path("cursor-agent").await.is_some()
476+
fn get_availability_info(&self) -> AvailabilityInfo {
477+
let binary_found = resolve_executable_path_blocking(Self::base_command()).is_some();
478+
if !binary_found {
479+
return AvailabilityInfo::NotFound;
480+
}
481+
482+
let config_files_found = self
483+
.default_mcp_config_path()
484+
.map(|p| p.exists())
485+
.unwrap_or(false);
486+
487+
if config_files_found {
488+
AvailabilityInfo::InstallationFound
489+
} else {
490+
AvailabilityInfo::NotFound
491+
}
477492
}
478493
}
479494
/* ===========================

crates/executors/src/executors/gemini.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ use workspace_utils::msg_store::MsgStore;
99
pub use super::acp::AcpAgentHarness;
1010
use crate::{
1111
command::{CmdOverrides, CommandBuilder, apply_overrides},
12-
executors::{AppendPrompt, ExecutorError, SpawnedChild, StandardCodingAgentExecutor},
12+
executors::{
13+
AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor,
14+
},
1315
};
1416

1517
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)]
@@ -75,4 +77,32 @@ impl StandardCodingAgentExecutor for Gemini {
7577
fn default_mcp_config_path(&self) -> Option<std::path::PathBuf> {
7678
dirs::home_dir().map(|home| home.join(".gemini").join("settings.json"))
7779
}
80+
81+
fn get_availability_info(&self) -> AvailabilityInfo {
82+
if let Some(timestamp) = dirs::home_dir()
83+
.and_then(|home| std::fs::metadata(home.join(".gemini").join("oauth_creds.json")).ok())
84+
.and_then(|m| m.modified().ok())
85+
.and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok())
86+
.map(|d| d.as_secs() as i64)
87+
{
88+
return AvailabilityInfo::LoginDetected {
89+
last_auth_timestamp: timestamp,
90+
};
91+
}
92+
93+
let mcp_config_found = self
94+
.default_mcp_config_path()
95+
.map(|p| p.exists())
96+
.unwrap_or(false);
97+
98+
let installation_indicator_found = dirs::home_dir()
99+
.map(|home| home.join(".gemini").join("installation_id").exists())
100+
.unwrap_or(false);
101+
102+
if mcp_config_found || installation_indicator_found {
103+
AvailabilityInfo::InstallationFound
104+
} else {
105+
AvailabilityInfo::NotFound
106+
}
107+
}
78108
}

crates/executors/src/executors/mod.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,24 @@ impl CodingAgent {
164164
}
165165
}
166166

167+
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
168+
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
169+
#[ts(export)]
170+
pub enum AvailabilityInfo {
171+
LoginDetected { last_auth_timestamp: i64 },
172+
InstallationFound,
173+
NotFound,
174+
}
175+
176+
impl AvailabilityInfo {
177+
pub fn is_available(&self) -> bool {
178+
matches!(
179+
self,
180+
AvailabilityInfo::LoginDetected { .. } | AvailabilityInfo::InstallationFound
181+
)
182+
}
183+
}
184+
167185
#[async_trait]
168186
#[enum_dispatch(CodingAgent)]
169187
pub trait StandardCodingAgentExecutor {
@@ -185,10 +203,17 @@ pub trait StandardCodingAgentExecutor {
185203
Err(ExecutorError::SetupHelperNotSupported)
186204
}
187205

188-
async fn check_availability(&self) -> bool {
189-
self.default_mcp_config_path()
206+
fn get_availability_info(&self) -> AvailabilityInfo {
207+
let config_files_found = self
208+
.default_mcp_config_path()
190209
.map(|path| path.exists())
191-
.unwrap_or(false)
210+
.unwrap_or(false);
211+
212+
if config_files_found {
213+
AvailabilityInfo::InstallationFound
214+
} else {
215+
AvailabilityInfo::NotFound
216+
}
192217
}
193218
}
194219

crates/executors/src/executors/opencode.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use workspace_utils::{msg_store::MsgStore, path::make_path_relative};
2121
use crate::{
2222
command::{CmdOverrides, CommandBuilder, apply_overrides},
2323
executors::{
24-
AppendPrompt, ExecutorError, SpawnedChild, StandardCodingAgentExecutor,
24+
AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor,
2525
opencode::share_bridge::Bridge as ShareBridge,
2626
},
2727
logs::{
@@ -310,6 +310,23 @@ impl StandardCodingAgentExecutor for Opencode {
310310
dirs::config_dir().map(|config| config.join("opencode").join("opencode.json"))
311311
}
312312
}
313+
314+
fn get_availability_info(&self) -> AvailabilityInfo {
315+
let mcp_config_found = self
316+
.default_mcp_config_path()
317+
.map(|p| p.exists())
318+
.unwrap_or(false);
319+
320+
let installation_indicator_found = dirs::config_dir()
321+
.map(|config| config.join("opencode").exists())
322+
.unwrap_or(false);
323+
324+
if mcp_config_found || installation_indicator_found {
325+
AvailabilityInfo::InstallationFound
326+
} else {
327+
AvailabilityInfo::NotFound
328+
}
329+
}
313330
}
314331
impl Opencode {
315332
const SHARE_PREFIX: &'static str = "[oc-share] ";

crates/executors/src/executors/qwen.rs

Lines changed: 18 additions & 1 deletion
< 3D20 td data-grid-cell-id="diff-f1ccf5458b1b236f24d9b093a891cb639ea21489cc7a6cf49eb0cbfe4084628d-71-79-2" data-line-anchor="diff-f1ccf5458b1b236f24d9b093a891cb639ea21489cc7a6cf49eb0cbfe4084628dR79" data-selected="false" role="gridcell" style="background-color:var(--diffBlob-additionLine-bgColor, var(--diffBlob-addition-bgColor-line));padding-right:24px" tabindex="-1" valign="top" class="focusable-grid-cell diff-text-cell right-side-diff-cell left-side">+
let installation_indicator_found = dirs::home_dir()
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use workspace_utils::msg_store::MsgStore;
99
use crate::{
1010
command::{CmdOverrides, CommandBuilder, apply_overrides},
1111
executors::{
12-
AppendPrompt, ExecutorError, SpawnedChild, StandardCodingAgentExecutor,
12+
AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor,
1313
gemini::AcpAgentHarness,
1414
},
1515
};
@@ -69,4 +69,21 @@ impl StandardCodingAgentExecutor for QwenCode {
6969
fn default_mcp_config_path(&self) -> Option<std::path::PathBuf> {
7070
dirs::home_dir().map(|home| home.join(".qwen").join("settings.json"))
7171
}
72+
73+
fn get_availability_info(&self) -> AvailabilityInfo {
74+
let mcp_config_found = self
75+
.default_mcp_config_path()
76+
.map(|p| p.exists())
77+
.unwrap_or(false);
78+
79
80+
.map(|home| home.join(".qwen").join("installation_id").exists())
81+
.unwrap_or(false);
82+
83+
if mcp_config_found || installation_indicator_found {
84+
AvailabilityInfo::InstallationFound
85+
} else {
86+
AvailabilityInfo::NotFound
87+
}
88+
}
7289
}

crates/executors/src/profile.rs

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
66
use thiserror::Error;
77
use ts_rs::TS;
88

9-
use crate::executors::{BaseCodingAgent, CodingAgent, StandardCodingAgentExecutor};
9+
use crate::executors::{
10+
AvailabilityInfo, BaseCodingAgent, CodingAgent, StandardCodingAgentExecutor,
11+
};
1012

1113
/// Return the canonical form for variant keys.
1214
/// – "DEFAULT" is kept as-is
@@ -409,20 +411,66 @@ impl ExecutorConfigs {
409411
.expect("No default variant found")
410412
})
411413
}
412-
/// Get the first available executor profile for new users
413414
pub async fn get_recommended_executor_profile(
414415
&self,
415416
) -> Result<ExecutorProfileId, ProfileError> {
417+
let mut agents_with_info: Vec<(BaseCodingAgent, AvailabilityInfo)> = Vec::new();
418+
416419
for &base_agent in self.executors.keys() {
417420
let profile_id = ExecutorProfileId::new(base_agent);
418-
if let Some(coding_agent) = self.get_coding_agent(&profile_id)
419-
&& coding_agent.check_availability().await
420-
{
421-
tracing::info!("Detected available executor: {}", base_agent);
422-
return Ok(profile_id);
421+
if let Some(coding_agent) = self.get_coding_agent(&profile_id) {
422+
let info = coding_agent.get_availability_info();
423+
if info.is_available() {
424+
agents_with_info.push((base_agent, info));
425+
}
423426
}
424427
}
425-
Err(ProfileError::NoAvailableExecutorProfile)
428+
429+
if agents_with_info.is_empty() {
430+
return Err(ProfileError::NoAvailableExecutorProfile);
431+
}
432+
433+
agents_with_info.sort_by(|a, b| {
434+
use crate::executors::AvailabilityInfo;
435+
match (&a.1, &b.1) {
436+
// Both have login detected - compare timestamps (most recent first)
437+
(
438+
AvailabilityInfo::LoginDetected {
439+
last_auth_timestamp: time_a,
440+
},
441+
AvailabilityInfo::LoginDetected {
442+
last_auth_timestamp: time_b,
443+
},
444+
) => time_b.cmp(time_a),
445+
// LoginDetected > InstallationFound
446+
(AvailabilityInfo::LoginDetected { .. }, AvailabilityInfo::InstallationFound) => {
447+
std::cmp::Ordering::Less
448+
}
449+
(AvailabilityInfo::InstallationFound, AvailabilityInfo::LoginDetected { .. }) => {
450+
std::cmp::Ordering::Greater
451+
}
452+
// LoginDetected > NotFound
453+
(AvailabilityInfo::LoginDetected { .. }, AvailabilityInfo::NotFound) => {
454+
std::cmp::Ordering::Less
455+
}
456+
(AvailabilityInfo::NotFound, AvailabilityInfo::LoginDetected { .. }) => {
457+
std::cmp::Ordering::Greater
458+
}
459+
// InstallationFound > NotFound
460+
(AvailabilityInfo::InstallationFound, AvailabilityInfo::NotFound) => {
461+
std::cmp::Ordering::Less
462+
}
463+
(AvailabilityInfo::NotFound, AvailabilityInfo::InstallationFound) => {
464+
std::cmp::Ordering::Greater
465+
}
466+
// Same state - equal
467+
_ => std::cmp::Ordering::Equal,
468+
}
469+
});
470+
471+
let selected = agents_with_info[0].0;
472+
tracing::info!("Recommended executor: {}", selected);
473+
Ok(ExecutorProfileId::new(selected))
426474
}
427475
}
428476

0 commit comments

Comments
 (0)
0