diff --git a/crates/executors/src/executors/claude.rs b/crates/executors/src/executors/claude.rs index bdcd92dc..6c8a75ac 100644 --- a/crates/executors/src/executors/claude.rs +++ b/crates/executors/src/executors/claude.rs @@ -22,7 +22,7 @@ use crate::{ approvals::ExecutorApprovalService, command::{CmdOverrides, CommandBuilder, CommandParts, apply_overrides}, executors::{ - AppendPrompt, ExecutorError, SpawnedChild, StandardCodingAgentExecutor, + AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor, codex::client::LogWriter, }, logs::{ @@ -193,6 +193,23 @@ impl StandardCodingAgentExecutor for ClaudeCode { fn default_mcp_config_path(&self) -> Option { dirs::home_dir().map(|home| home.join(".claude.json")) } + + fn get_availability_info(&self) -> AvailabilityInfo { + let auth_file_path = dirs::home_dir().map(|home| home.join(".claude.json")); + + if let Some(path) = auth_file_path + && let Some(timestamp) = std::fs::metadata(&path) + .ok() + .and_then(|m| m.modified().ok()) + .and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|d| d.as_secs() as i64) + { + return AvailabilityInfo::LoginDetected { + last_auth_timestamp: timestamp, + }; + } + AvailabilityInfo::NotFound + } } impl ClaudeCode { diff --git a/crates/executors/src/executors/codex.rs b/crates/executors/src/executors/codex.rs index 3ac88d8d..f5a4a515 100644 --- a/crates/executors/src/executors/codex.rs +++ b/crates/executors/src/executors/codex.rs @@ -33,7 +33,7 @@ use crate::{ approvals::ExecutorApprovalService, command::{CmdOverrides, CommandBuilder, CommandParts, apply_overrides}, executors::{ - AppendPrompt, ExecutorError, SpawnedChild, StandardCodingAgentExecutor, + AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor, codex::{jsonrpc::ExitSignalSender, normalize_logs::Error}, }, stdout_dup::create_stdout_pipe_writer, @@ -164,6 +164,34 @@ impl StandardCodingAgentExecutor for Codex { fn default_mcp_config_path(&self) -> Option { dirs::home_dir().map(|home| home.join(".codex").join("config.toml")) } + + fn get_availability_info(&self) -> AvailabilityInfo { + if let Some(timestamp) = dirs::home_dir() + .and_then(|home| std::fs::metadata(home.join(".codex").join("auth.json")).ok()) + .and_then(|m| m.modified().ok()) + .and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|d| d.as_secs() as i64) + { + return AvailabilityInfo::LoginDetected { + last_auth_timestamp: timestamp, + }; + } + + let mcp_config_found = self + .default_mcp_config_path() + .map(|p| p.exists()) + .unwrap_or(false); + + let installation_indicator_found = dirs::home_dir() + .map(|home| home.join(".codex").join("version.json").exists()) + .unwrap_or(false); + + if mcp_config_found || installation_indicator_found { + AvailabilityInfo::InstallationFound + } else { + AvailabilityInfo::NotFound + } + } } impl Codex { diff --git a/crates/executors/src/executors/copilot.rs b/crates/executors/src/executors/copilot.rs index 885f8791..56506014 100644 --- a/crates/executors/src/executors/copilot.rs +++ b/crates/executors/src/executors/copilot.rs @@ -23,7 +23,9 @@ use workspace_utils::{msg_store::MsgStore, path::get_vibe_kanban_temp_dir}; use crate::{ command::{CmdOverrides, CommandBuilder, apply_overrides}, - executors::{AppendPrompt, ExecutorError, SpawnedChild, StandardCodingAgentExecutor}, + executors::{ + AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor, + }, logs::{ NormalizedEntry, NormalizedEntryType, plain_text_processor::PlainTextLogProcessor, stderr_processor::normalize_stderr_logs, utils::EntryIndexProvider, @@ -197,6 +199,23 @@ impl StandardCodingAgentExecutor for Copilot { fn default_mcp_config_path(&self) -> Option { dirs::home_dir().map(|home| home.join(".copilot").join("mcp-config.json")) } + + fn get_availability_info(&self) -> AvailabilityInfo { + let mcp_config_found = self + .default_mcp_config_path() + .map(|p| p.exists()) + .unwrap_or(false); + + let installation_indicator_found = dirs::home_dir() + .map(|home| home.join(".copilot").join("config.json").exists()) + .unwrap_or(false); + + if mcp_config_found || installation_indicator_found { + AvailabilityInfo::InstallationFound + } else { + AvailabilityInfo::NotFound + } + } } impl Copilot { diff --git a/crates/executors/src/executors/cursor.rs b/crates/executors/src/executors/cursor.rs index fd8f388f..0289e188 100644 --- a/crates/executors/src/executors/cursor.rs +++ b/crates/executors/src/executors/cursor.rs @@ -12,12 +12,14 @@ use workspace_utils::{ diff::{concatenate_diff_hunks, create_unified_diff, extract_unified_diff_hunks}, msg_store::MsgStore, path::make_path_relative, - shell::resolve_executable_path, + shell::resolve_executable_path_blocking, }; use crate::{ command::{CmdOverrides, CommandBuilder, apply_overrides}, - executors::{AppendPrompt, ExecutorError, SpawnedChild, StandardCodingAgentExecutor}, + executors::{ + AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor, + }, logs::{ ActionType, FileChange, NormalizedEntry, NormalizedEntryError, NormalizedEntryType, TodoItem, ToolStatus, @@ -467,13 +469,26 @@ impl StandardCodingAgentExecutor for CursorAgent { }); } - // MCP configuration methods fn default_mcp_config_path(&self) -> Option { dirs::home_dir().map(|home| home.join(".cursor").join("mcp.json")) } - async fn check_availability(&self) -> bool { - resolve_executable_path("cursor-agent").await.is_some() + fn get_availability_info(&self) -> AvailabilityInfo { + let binary_found = resolve_executable_path_blocking(Self::base_command()).is_some(); + if !binary_found { + return AvailabilityInfo::NotFound; + } + + let config_files_found = self + .default_mcp_config_path() + .map(|p| p.exists()) + .unwrap_or(false); + + if config_files_found { + AvailabilityInfo::InstallationFound + } else { + AvailabilityInfo::NotFound + } } } /* =========================== diff --git a/crates/executors/src/executors/gemini.rs b/crates/executors/src/executors/gemini.rs index ca6e9602..4e494152 100644 --- a/crates/executors/src/executors/gemini.rs +++ b/crates/executors/src/executors/gemini.rs @@ -9,7 +9,9 @@ use workspace_utils::msg_store::MsgStore; pub use super::acp::AcpAgentHarness; use crate::{ command::{CmdOverrides, CommandBuilder, apply_overrides}, - executors::{AppendPrompt, ExecutorError, SpawnedChild, StandardCodingAgentExecutor}, + executors::{ + AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor, + }, }; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, TS, JsonSchema)] @@ -75,4 +77,32 @@ impl StandardCodingAgentExecutor for Gemini { fn default_mcp_config_path(&self) -> Option { dirs::home_dir().map(|home| home.join(".gemini").join("settings.json")) } + + fn get_availability_info(&self) -> AvailabilityInfo { + if let Some(timestamp) = dirs::home_dir() + .and_then(|home| std::fs::metadata(home.join(".gemini").join("oauth_creds.json")).ok()) + .and_then(|m| m.modified().ok()) + .and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok()) + .map(|d| d.as_secs() as i64) + { + return AvailabilityInfo::LoginDetected { + last_auth_timestamp: timestamp, + }; + } + + let mcp_config_found = self + .default_mcp_config_path() + .map(|p| p.exists()) + .unwrap_or(false); + + let installation_indicator_found = dirs::home_dir() + .map(|home| home.join(".gemini").join("installation_id").exists()) + .unwrap_or(false); + + if mcp_config_found || installation_indicator_found { + AvailabilityInfo::InstallationFound + } else { + AvailabilityInfo::NotFound + } + } } diff --git a/crates/executors/src/executors/mod.rs b/crates/executors/src/executors/mod.rs index 7158c67f..809df34e 100644 --- a/crates/executors/src/executors/mod.rs +++ b/crates/executors/src/executors/mod.rs @@ -164,6 +164,24 @@ impl CodingAgent { } } +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")] +#[ts(export)] +pub enum AvailabilityInfo { + LoginDetected { last_auth_timestamp: i64 }, + InstallationFound, + NotFound, +} + +impl AvailabilityInfo { + pub fn is_available(&self) -> bool { + matches!( + self, + AvailabilityInfo::LoginDetected { .. } | AvailabilityInfo::InstallationFound + ) + } +} + #[async_trait] #[enum_dispatch(CodingAgent)] pub trait StandardCodingAgentExecutor { @@ -185,10 +203,17 @@ pub trait StandardCodingAgentExecutor { Err(ExecutorError::SetupHelperNotSupported) } - async fn check_availability(&self) -> bool { - self.default_mcp_config_path() + fn get_availability_info(&self) -> AvailabilityInfo { + let config_files_found = self + .default_mcp_config_path() .map(|path| path.exists()) - .unwrap_or(false) + .unwrap_or(false); + + if config_files_found { + AvailabilityInfo::InstallationFound + } else { + AvailabilityInfo::NotFound + } } } diff --git a/crates/executors/src/executors/opencode.rs b/crates/executors/src/executors/opencode.rs index 5c60249d..b52586a0 100644 --- a/crates/executors/src/executors/opencode.rs +++ b/crates/executors/src/executors/opencode.rs @@ -21,7 +21,7 @@ use workspace_utils::{msg_store::MsgStore, path::make_path_relative}; use crate::{ command::{CmdOverrides, CommandBuilder, apply_overrides}, executors::{ - AppendPrompt, ExecutorError, SpawnedChild, StandardCodingAgentExecutor, + AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor, opencode::share_bridge::Bridge as ShareBridge, }, logs::{ @@ -310,6 +310,23 @@ impl StandardCodingAgentExecutor for Opencode { dirs::config_dir().map(|config| config.join("opencode").join("opencode.json")) } } + + fn get_availability_info(&self) -> AvailabilityInfo { + let mcp_config_found = self + .default_mcp_config_path() + .map(|p| p.exists()) + .unwrap_or(false); + + let installation_indicator_found = dirs::config_dir() + .map(|config| config.join("opencode").exists()) + .unwrap_or(false); + + if mcp_config_found || installation_indicator_found { + AvailabilityInfo::InstallationFound + } else { + AvailabilityInfo::NotFound + } + } } impl Opencode { const SHARE_PREFIX: &'static str = "[oc-share] "; diff --git a/crates/executors/src/executors/qwen.rs b/crates/executors/src/executors/qwen.rs index d3b5f2ce..b008e56d 100644 --- a/crates/executors/src/executors/qwen.rs +++ b/crates/executors/src/executors/qwen.rs @@ -9,7 +9,7 @@ use workspace_utils::msg_store::MsgStore; use crate::{ command::{CmdOverrides, CommandBuilder, apply_overrides}, executors::{ - AppendPrompt, ExecutorError, SpawnedChild, StandardCodingAgentExecutor, + AppendPrompt, AvailabilityInfo, ExecutorError, SpawnedChild, StandardCodingAgentExecutor, gemini::AcpAgentHarness, }, }; @@ -69,4 +69,21 @@ impl StandardCodingAgentExecutor for QwenCode { fn default_mcp_config_path(&self) -> Option { dirs::home_dir().map(|home| home.join(".qwen").join("settings.json")) } + + fn get_availability_info(&self) -> AvailabilityInfo { + let mcp_config_found = self + .default_mcp_config_path() + .map(|p| p.exists()) + .unwrap_or(false); + + let installation_indicator_found = dirs::home_dir() + .map(|home| home.join(".qwen").join("installation_id").exists()) + .unwrap_or(false); + + if mcp_config_found || installation_indicator_found { + AvailabilityInfo::InstallationFound + } else { + AvailabilityInfo::NotFound + } + } } diff --git a/crates/executors/src/profile.rs b/crates/executors/src/profile.rs index 188370df..e3284562 100644 --- a/crates/executors/src/profile.rs +++ b/crates/executors/src/profile.rs @@ -6,7 +6,9 @@ use serde::{Deserialize, Deserializer, Serialize, de::Error as DeError}; use thiserror::Error; use ts_rs::TS; -use crate::executors::{BaseCodingAgent, CodingAgent, StandardCodingAgentExecutor}; +use crate::executors::{ + AvailabilityInfo, BaseCodingAgent, CodingAgent, StandardCodingAgentExecutor, +}; /// Return the canonical form for variant keys. /// – "DEFAULT" is kept as-is @@ -409,20 +411,66 @@ 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 { + let mut agents_with_info: Vec<(BaseCodingAgent, AvailabilityInfo)> = Vec::new(); + 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); + if let Some(coding_agent) = self.get_coding_agent(&profile_id) { + let info = coding_agent.get_availability_info(); + if info.is_available() { + agents_with_info.push((base_agent, info)); + } } } - Err(ProfileError::NoAvailableExecutorProfile) + + if agents_with_info.is_empty() { + return Err(ProfileError::NoAvailableExecutorProfile); + } + + agents_with_info.sort_by(|a, b| { + use crate::executors::AvailabilityInfo; + match (&a.1, &b.1) { + // Both have login detected - compare timestamps (most recent first) + ( + AvailabilityInfo::LoginDetected { + last_auth_timestamp: time_a, + }, + AvailabilityInfo::LoginDetected { + last_auth_timestamp: time_b, + }, + ) => time_b.cmp(time_a), + // LoginDetected > InstallationFound + (AvailabilityInfo::LoginDetected { .. }, AvailabilityInfo::InstallationFound) => { + std::cmp::Ordering::Less + } + (AvailabilityInfo::InstallationFound, AvailabilityInfo::LoginDetected { .. }) => { + std::cmp::Ordering::Greater + } + // LoginDetected > NotFound + (AvailabilityInfo::LoginDetected { .. }, AvailabilityInfo::NotFound) => { + std::cmp::Ordering::Less + } + (AvailabilityInfo::NotFound, AvailabilityInfo::LoginDetected { .. }) => { + std::cmp::Ordering::Greater + } + // InstallationFound > NotFound + (AvailabilityInfo::InstallationFound, AvailabilityInfo::NotFound) => { + std::cmp::Ordering::Less + } + (AvailabilityInfo::NotFound, AvailabilityInfo::InstallationFound) => { + std::cmp::Ordering::Greater + } + // Same state - equal + _ => std::cmp::Ordering::Equal, + } + }); + + let selected = agents_with_info[0].0; + tracing::info!("Recommended executor: {}", selected); + Ok(ExecutorProfileId::new(selected)) } } diff --git a/crates/server/src/bin/generate_types.rs b/crates/server/src/bin/generate_types.rs index b6f43eca..5fd20e75 100644 --- a/crates/server/src/bin/generate_types.rs +++ b/crates/server/src/bin/generate_types.rs @@ -76,6 +76,8 @@ fn generate_types_content() -> String { server::routes::config::GetMcpServerResponse::decl(), server::routes::config::CheckEditorAvailabilityQuery::decl(), server::routes::config::CheckEditorAvailabilityResponse::decl(), + server::routes::config::CheckAgentAvailabilityQuery::decl(), + executors::executors::AvailabilityInfo::decl(), server::routes::task_attempts::CreateFollowUpAttempt::decl(), services::services::drafts::DraftResponse::decl(), services::services::drafts::UpdateFollowUpDraftRequest::decl(), diff --git a/crates/server/src/routes/config.rs b/crates/server/src/routes/config.rs index f5548b08..46252898 100644 --- a/crates/server/src/routes/config.rs +++ b/crates/server/src/routes/config.rs @@ -10,7 +10,9 @@ use axum::{ }; use deployment::{Deployment, DeploymentError}; use executors::{ - executors::{BaseAgentCapability, BaseCodingAgent, StandardCodingAgentExecutor}, + executors::{ + AvailabilityInfo, BaseAgentCapability, BaseCodingAgent, StandardCodingAgentExecutor, + }, mcp_config::{McpConfig, read_agent_config, write_agent_config}, profile::{ExecutorConfigs, ExecutorProfileId}, }; @@ -38,6 +40,7 @@ pub fn router() -> Router { "/editors/check-availability", get(check_editor_availability), ) + .route("/agents/check-availability", get(check_agent_availability)) } #[derive(Debug, Serialize, Deserialize, TS)] @@ -462,3 +465,23 @@ async fn check_editor_availability( available, })) } + +#[derive(Debug, Serialize, Deserialize, TS)] +pub struct CheckAgentAvailabilityQuery { + executor: BaseCodingAgent, +} + +async fn check_agent_availability( + State(_deployment): State, + Query(query): Query, +) -> ResponseJson> { + let profiles = ExecutorConfigs::get_cached(); + let profile_id = ExecutorProfileId::new(query.executor); + + let info = match profiles.get_coding_agent(&profile_id) { + Some(agent) => agent.get_availability_info(), + None => AvailabilityInfo::NotFound, + }; + + ResponseJson(ApiResponse::success(info)) +} diff --git a/frontend/src/components/AgentAvailabilityIndicator.tsx b/frontend/src/components/AgentAvailabilityIndicator.tsx new file mode 100644 index 00000000..bdcd2c24 --- /dev/null +++ b/frontend/src/components/AgentAvailabilityIndicator.tsx @@ -0,0 +1,67 @@ +import { Check, AlertCircle, Loader2 } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import type { AgentAvailabilityState } from '@/hooks/useAgentAvailability'; + +interface AgentAvailabilityIndicatorProps { + availability: AgentAvailabilityState; +} + +export function AgentAvailabilityIndicator({ + availability, +}: AgentAvailabilityIndicatorProps) { + const { t } = useTranslation('settings'); + + if (!availability) return null; + + return ( +
+ {availability.status === 'checking' && ( +
+ + + {t('settings.agents.availability.checking')} + +
+ )} + {availability.status === 'login_detected' && ( + <> +
+ + + {t('settings.agents.availability.loginDetected')} + +
+

+ {t('settings.agents.availability.loginDetectedTooltip')} +

+ + )} + {availability.status === 'installation_found' && ( + <> +
+ + + {t('settings.agents.availability.installationFound')} + +
+

+ {t('settings.agents.availability.installationFoundTooltip')} +

+ + )} + {availability.status === 'not_found' && ( + <> +
+ + + {t('settings.agents.availability.notFound')} + +
+

+ {t('settings.agents.availability.notFoundTooltip')} +

+ + )} +
+ ); +} diff --git a/frontend/src/components/EditorAvailabilityIndicator.tsx b/frontend/src/components/EditorAvailabilityIndicator.tsx index 2b3f387b..bab1ad8e 100644 --- a/frontend/src/components/EditorAvailabilityIndicator.tsx +++ b/frontend/src/components/EditorAvailabilityIndicator.tsx @@ -29,16 +29,16 @@ export function EditorAvailabilityIndicator({ )} {availability === 'available' && ( <> - - + + {t('settings.general.editor.availability.available')} )} {availability === 'unavailable' && ( <> - - + + {t('settings.general.editor.availability.notFound')} diff --git a/frontend/src/components/dialogs/global/OnboardingDialog.tsx b/frontend/src/components/dialogs/global/OnboardingDialog.tsx index eac72aee..86209fdd 100644 --- a/frontend/src/components/dialogs/global/OnboardingDialog.tsx +++ b/frontend/src/components/dialogs/global/OnboardingDialog.tsx @@ -33,6 +33,8 @@ import NiceModal, { useModal } from '@ebay/nice-modal-react'; import { defineModal, type NoProps } from '@/lib/modals'; import { useEditorAvailability } from '@/hooks/useEditorAvailability'; import { EditorAvailabilityIndicator } from '@/components/EditorAvailabilityIndicator'; +import { useAgentAvailability } from '@/hooks/useAgentAvailability'; +import { AgentAvailabilityIndicator } from '@/components/AgentAvailabilityIndicator'; export type OnboardingResult = { profile: ExecutorProfileId; @@ -52,8 +54,8 @@ const OnboardingDialogImpl = NiceModal.create(() => { const [editorType, setEditorType] = useState(EditorType.VS_CODE); const [customCommand, setCustomCommand] = useState(''); - // Check editor availability when selection changes const editorAvailability = useEditorAvailability(editorType); + const agentAvailability = useAgentAvailability(profile.executor); const handleComplete = () => { modal.resolve({ @@ -104,13 +106,13 @@ const OnboardingDialogImpl = NiceModal.create(() => { {profiles && - (Object.keys(profiles) as BaseCodingAgent[]).map( - (agent) => ( + (Object.keys(profiles) as BaseCodingAgent[]) + .sort() + .map((agent) => ( {agent} - ) - )} + ))} @@ -171,6 +173,7 @@ const OnboardingDialogImpl = NiceModal.create(() => { return null; })()} + diff --git a/frontend/src/hooks/useAgentAvailability.ts b/frontend/src/hooks/useAgentAvailability.ts new file mode 100644 index 00000000..4965ebe0 --- /dev/null +++ b/frontend/src/hooks/useAgentAvailability.ts @@ -0,0 +1,51 @@ +import { useEffect, useState } from 'react'; +import { BaseCodingAgent } from 'shared/types'; +import { configApi } from '../lib/api'; + +export type AgentAvailabilityState = + | { status: 'checking' } + | { status: 'login_detected' } + | { status: 'installation_found' } + | { status: 'not_found' } + | null; + +export function useAgentAvailability( + agent: BaseCodingAgent | null | undefined +): AgentAvailabilityState { + const [availability, setAvailability] = + useState(null); + + useEffect(() => { + if (!agent) { + setAvailability(null); + return; + } + + const checkAvailability = async () => { + setAvailability({ status: 'checking' }); + try { + const info = await configApi.checkAgentAvailability(agent); + + // Map backend enum to frontend state + switch (info.type) { + case 'LOGIN_DETECTED': + setAvailability({ status: 'login_detected' }); + break; + case 'INSTALLATION_FOUND': + setAvailability({ status: 'installation_found' }); + break; + case 'NOT_FOUND': + setAvailability({ status: 'not_found' }); + break; + } + } catch (error) { + console.error('Failed to check agent availability:', error); + setAvailability(null); + } + }; + + checkAvailability(); + }, [agent]); + + return availability; +} diff --git a/frontend/src/i18n/locales/en/settings.json b/frontend/src/i18n/locales/en/settings.json index 12b9f31f..fb4fa46e 100644 --- a/frontend/src/i18n/locales/en/settings.json +++ b/frontend/src/i18n/locales/en/settings.json @@ -228,6 +228,15 @@ "button": "Save Agent Configurations", "success": "✓ Executor configurations saved successfully!" }, + "availability": { + "checking": "Checking...", + "loginDetected": "Recent Usage Detected", + "loginDetectedTooltip": "Recent authentication credentials found for this agent", + "installationFound": "Previous Usage Detected", + "installationFoundTooltip": "Agent configuration found. You may need to log in to use it.", + "notFound": "Not Found", + "notFoundTooltip": "No previous usage detected. Agent may require installation and/or login." + }, "editor": { "formLabel": "Edit JSON", "agentLabel": "Agent", diff --git a/frontend/src/i18n/locales/es/settings.json b/frontend/src/i18n/locales/es/settings.json index e5e63059..21c35041 100644 --- a/frontend/src/i18n/locales/es/settings.json +++ b/frontend/src/i18n/locales/es/settings.json @@ -226,7 +226,16 @@ "loading": "Cargando configuraciones de agentes...", "save": { "button": "Guardar Configuraciones de Agentes", - "success": "✓ ¡Configuración guardada con éxito”!" + "success": "✓ ¡Configuración guardada con éxito!" + }, + "availability": { + "checking": "Comprobando...", + "loginDetected": "Uso reciente detectado", + "loginDetectedTooltip": "Se encontraron credenciales de autenticación recientes para este agente", + "installationFound": "Uso previo detectado", + "installationFoundTooltip": "Se encontró la configuración del agente. Es posible que debas iniciar sesión para usarlo.", + "notFound": "No encontrado", + "notFoundTooltip": "No se detectó uso previo. El agente puede requerir instalación y/o inicio de sesión." }, "editor": { "formLabel": "Editar JSON", diff --git a/frontend/src/i18n/locales/ja/settings.json b/frontend/src/i18n/locales/ja/settings.json index dd63be32..97c264a0 100644 --- a/frontend/src/i18n/locales/ja/settings.json +++ b/frontend/src/i18n/locales/ja/settings.json @@ -228,6 +228,15 @@ "button": "エージェント設定を保存", "success": "✓ 実行設定が正常に保存されました!" }, + "availability": { + "checking": "確認中...", + "loginDetected": "最近の使用を検出", + "loginDetectedTooltip": "このエージェントの最近の認証情報が見つかりました", + "installationFound": "以前の使用を検出", + "installationFoundTooltip": "エージェント設定が見つかりました。使用するにはログインが必要な場合があります。", + "notFound": "見つかりません", + "notFoundTooltip": "以前の使用が検出されませんでした。エージェントにはインストールやログインが必要な場合があります。" + }, "editor": { "formLabel": "JSONを編集", "agentLabel": "エージェント", diff --git a/frontend/src/i18n/locales/ko/settings.json b/frontend/src/i18n/locales/ko/settings.json index 9e4b2a8a..3fa66277 100644 --- a/frontend/src/i18n/locales/ko/settings.json +++ b/frontend/src/i18n/locales/ko/settings.json @@ -228,6 +228,15 @@ "button": "에이전트 구성 저장", "success": "✓ 실행자 구성이 성공적으로 저장되었습니다!" }, + "availability": { + "checking": "확인 중...", + "loginDetected": "최근 사용 감지됨", + "loginDetectedTooltip": "이 에이전트에 대한 최근 인증 자격 증명이 발견되었습니다", + "installationFound": "이전 사용 감지됨", + "installationFoundTooltip": "에이전트 구성이 발견되었습니다. 사용하려면 로그인해야 할 수 있습니다.", + "notFound": "찾을 수 없음", + "notFoundTooltip": "이전 사용이 감지되지 않았습니다. 에이전트에 설치 및/또는 로그인이 필요할 수 있습니다." + }, "editor": { "formLabel": "JSON 편집", "agentLabel": "에이전트", diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 603625b4..f64335a8 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -47,6 +47,8 @@ import { RenameBranchRequest, RenameBranchResponse, CheckEditorAvailabilityResponse, + AvailabilityInfo, + BaseCodingAgent, RunAgentSetupRequest, RunAgentSetupResponse, GhCliSetupError, @@ -736,6 +738,14 @@ export const configApi = { ); return handleApiResponse(response); }, + checkAgentAvailability: async ( + agent: BaseCodingAgent + ): Promise => { + const response = await makeRequest( + `/api/agents/check-availability?executor=${encodeURIComponent(agent)}` + ); + return handleApiResponse(response); + }, }; // Task Tags APIs (all tags are global) diff --git a/frontend/src/pages/settings/GeneralSettings.tsx b/frontend/src/pages/settings/GeneralSettings.tsx index e3ea1177..7cb28c19 100644 --- a/frontend/src/pages/settings/GeneralSettings.tsx +++ b/frontend/src/pages/settings/GeneralSettings.tsx @@ -40,6 +40,8 @@ import { getLanguageOptions } from '@/i18n/languages'; import { toPrettyCase } from '@/utils/string'; import { useEditorAvailability } from '@/hooks/useEditorAvailability'; import { EditorAvailabilityIndicator } from '@/components/EditorAvailabilityIndicator'; +import { useAgentAvailability } from '@/hooks/useAgentAvailability'; +import { AgentAvailabilityIndicator } from '@/components/AgentAvailabilityIndicator'; import { useTheme } from '@/components/ThemeProvider'; import { useUserSystem } from '@/components/ConfigProvider'; import { TagManager } from '@/components/TagManager'; @@ -75,6 +77,11 @@ export function GeneralSettings() { // Check editor availability when draft editor changes const editorAvailability = useEditorAvailability(draft?.editor.editor_type); + // Check agent availability when draft executor changes + const agentAvailability = useAgentAvailability( + draft?.executor_profile?.executor + ); + const validateBranchPrefix = useCallback( (prefix: string): string | null => { if (!prefix) return null; // empty allowed @@ -412,6 +419,7 @@ export function GeneralSettings() { return null; })()} +

{t('settings.general.taskExecution.executor.helper')}

diff --git a/shared/types.ts b/shared/types.ts index 16bf7fa4..8af8d0e3 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -140,6 +140,10 @@ export type CheckEditorAvailabilityQuery = { editor_type: EditorType, }; export type CheckEditorAvailabilityResponse = { available: boolean, }; +export type CheckAgentAvailabilityQuery = { executor: BaseCodingAgent, }; + +export type AvailabilityInfo = { "type": "LOGIN_DETECTED", last_auth_timestamp: bigint, } | { "type": "INSTALLATION_FOUND" } | { "type": "NOT_FOUND" }; + export type CreateFollowUpAttempt = { prompt: string, variant: string | null, image_ids: Array | null, retry_process_id: string | null, force_when_dirty: boolean | null, perform_git_reset: boolean | null, }; export type DraftResponse = { task_attempt_id: string, draft_type: DraftType, retry_process_id: string | null, prompt: string, queued: boolean, variant: string | null, image_ids: Array | null, version: bigint, };