Find installed executors (vibe-kanban) (#610)
* feat: add availability check for coding agents and set smart defaults for new users
* Add executor discovery (vibe-kanban d65d61aa)
Plan: Add executor discovery with appropriate checks
Understanding
You're absolutely right! Checking if npx is available is useless for Opencode/Qwen since it doesn't tell us if those specific agents have been set up. We should follow the pattern used by other executors:
Binary-based executors (like Cursor): Check if the binary exists
NPX-based executors (like Claude, Gemini, Opencode, Qwen): Check for their settings files
Implementation Plan
1. Cursor executor (cursor.rs, after line 407)
Since Cursor uses a standalone binary:
async fn check_availability(&self) -> bool {
utils::shell::resolve_executable_path("cursor-agent").is_some()
}
Import needed: Add shell::resolve_executable_path to the utils import
2. Opencode executor (opencode.rs, after line 188)
Follow the pattern of other NPX-based executors - check for settings file:
async fn check_availability(&self) -> bool {
self.default_mcp_config_path()
.map(|path| path.exists())
.unwrap_or(false)
}
This will check for the opencode.json file in the appropriate location (XDG on Unix, config_dir on Windows).
3. QwenCode executor (qwen.rs, after line 140)
Same approach as other NPX-based executors:
async fn check_availability(&self) -> bool {
self.default_mcp_config_path()
.map(|path| path.exists())
.unwrap_or(false)
}
This will check for ~/.qwen/settings.json.
Why this approach is correct:
Cursor: Has a specific binary that must be installed → check for binary
Opencode/Qwen: Are NPX-based like Claude/Gemini → check for settings files
Consistency: Follows the existing pattern where NPX-based executors check for settings files
Meaningful detection: Settings files indicate the agent has been configured/used before, which is what we want for "availability"
This gives us meaningful availability detection that matches the existing patterns in the codebase.
* Remove duplicated config paths (vibe-kanban 207e0410)
In some executors, like crates/executors/src/executors/amp.rs , Paths for checking availability and mcp server are identical but not reused. We should not duplicate code. Consider changing the default availability check to just check for the mcp config
* feat: implement recommended executor profile retrieval for new users
* fmt
This commit is contained in:
@@ -14,7 +14,7 @@ use utils::{
|
||||
},
|
||||
msg_store::MsgStore,
|
||||
path::make_path_relative,
|
||||
shell::get_shell_command,
|
||||
shell::{get_shell_command, resolve_executable_path},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
@@ -405,6 +405,10 @@ impl StandardCodingAgentExecutor for Cursor {
|
||||
fn default_mcp_config_path(&self) -> Option<std::path::PathBuf> {
|
||||
dirs::home_dir().map(|home| home.join(".cursor").join("mcp.json"))
|
||||
}
|
||||
|
||||
async fn check_availability(&self) -> bool {
|
||||
resolve_executable_path("cursor-agent").is_some()
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_cursor_ascii_art_banner(line: String) -> String {
|
||||
|
||||
@@ -145,4 +145,10 @@ pub trait StandardCodingAgentExecutor {
|
||||
|
||||
// MCP configuration methods
|
||||
fn default_mcp_config_path(&self) -> Option<std::path::PathBuf>;
|
||||
|
||||
async fn check_availability(&self) -> bool {
|
||||
self.default_mcp_config_path()
|
||||
.map(|path| path.exists())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError};
|
||||
use thiserror::Error;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::executors::{BaseCodingAgent, CodingAgent};
|
||||
use crate::executors::{BaseCodingAgent, CodingAgent, StandardCodingAgentExecutor};
|
||||
|
||||
/// Return the canonical form for variant keys.
|
||||
/// – "DEFAULT" is kept as-is
|
||||
@@ -40,6 +40,9 @@ pub enum ProfileError {
|
||||
|
||||
#[error(transparent)]
|
||||
Serde(#[from] serde_json::Error),
|
||||
|
||||
#[error("No available executor profile")]
|
||||
NoAvailableExecutorProfile,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
@@ -406,4 +409,19 @@ impl ExecutorConfigs {
|
||||
.expect("No default variant found")
|
||||
})
|
||||
}
|
||||
/// Get the first available executor profile for new users
|
||||
pub async fn get_recommended_executor_profile(
|
||||
&self,
|
||||
) -> Result<ExecutorProfileId, ProfileError> {
|
||||
for &base_agent in self.executors.keys() {
|
||||
let profile_id = ExecutorProfileId::new(base_agent);
|
||||
if let Some(coding_agent) = self.get_coding_agent(&profile_id)
|
||||
&& coding_agent.check_availability().await
|
||||
{
|
||||
tracing::info!("Detected available executor: {}", base_agent);
|
||||
return Ok(profile_id);
|
||||
}
|
||||
}
|
||||
Err(ProfileError::NoAvailableExecutorProfile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::{collections::HashMap, sync::Arc};
|
||||
use async_trait::async_trait;
|
||||
use db::DBService;
|
||||
use deployment::{Deployment, DeploymentError};
|
||||
use executors::profile::ExecutorConfigs;
|
||||
use services::services::{
|
||||
analytics::{AnalyticsConfig, AnalyticsContext, AnalyticsService, generate_user_id},
|
||||
auth::AuthService,
|
||||
@@ -44,6 +45,13 @@ impl Deployment for LocalDeployment {
|
||||
async fn new() -> Result<Self, DeploymentError> {
|
||||
let mut raw_config = load_config_from_file(&config_path()).await;
|
||||
|
||||
let profiles = ExecutorConfigs::get_cached();
|
||||
if !raw_config.onboarding_acknowledged
|
||||
&& let Ok(recommended_executor) = profiles.get_recommended_executor_profile().await
|
||||
{
|
||||
raw_config.executor_profile = recommended_executor;
|
||||
}
|
||||
|
||||
// Check if app version has changed and set release notes flag
|
||||
{
|
||||
let current_version = utils::version::APP_VERSION;
|
||||
|
||||
@@ -39,15 +39,17 @@ interface OnboardingDialogProps {
|
||||
}
|
||||
|
||||
export function OnboardingDialog({ open, onComplete }: OnboardingDialogProps) {
|
||||
const [profile, setProfile] = useState<ExecutorProfileId>({
|
||||
executor: BaseCodingAgent.CLAUDE_CODE,
|
||||
variant: null,
|
||||
});
|
||||
const { profiles, config } = useUserSystem();
|
||||
|
||||
const [profile, setProfile] = useState<ExecutorProfileId>(
|
||||
config?.executor_profile || {
|
||||
executor: BaseCodingAgent.CLAUDE_CODE,
|
||||
variant: null,
|
||||
}
|
||||
);
|
||||
const [editorType, setEditorType] = useState<EditorType>(EditorType.VS_CODE);
|
||||
const [customCommand, setCustomCommand] = useState<string>('');
|
||||
|
||||
const { profiles } = useUserSystem();
|
||||
|
||||
const handleComplete = () => {
|
||||
onComplete({
|
||||
profile,
|
||||
|
||||
Reference in New Issue
Block a user