feat: edit coding agent profiles (#453)
* edit profiles.json * move default crate configuration to a default_profiles.json button to open mcp config in editor initialse empty mcp config files fix test new JSON structure remove editor buttons fmt and types * feat: add profile field to follow-up attempt (#442) * move default crate configuration to a default_profiles.json * new JSON structure * feat: add profile field to follow-up attempt; fix follow ups using wrong session id at 2nd+ follow up fmt Profile selection (vibe-kanban cf714482) Right now in the frontend, when viewing a task card, we show the base_coding_agent from the task attempt. We should also show the currently selected profile there in the same way feat: add watchkill support to CommandBuilder and integrate with Claude executor feat: refactor profile handling to use ProfileVariant across executors and requests feat: restructure command modes in default_profiles.json for clarity and consistency update profile handling to use ProfileVariant across components and add mode selection fmt feat: refactor profile handling to use variants instead of modes across components and update related structures Fix frontend * Refactor coding agent representation in task and task attempt models - Changed `base_coding_agent` field to `profile` in `TaskWithAttemptStatus` and `TaskAttempt` structs. - Updated SQL queries and data handling to reflect the new `profile` field. - Modified related API endpoints and request/response structures to use `profile` instead of `base_coding_agent`. - Adjusted frontend API calls and components to align with the updated data structure. - Removed unused `BaseCodingAgent` enum and related type guards from the frontend. - Enhanced MCP server configuration handling to utilize the new profile-based approach. feat: Introduce MCP configuration management - Added `McpConfig` struct for managing MCP server configurations. - Implemented reading and writing of agent config files in JSON and TOML formats. - Refactored MCP server handling in the `McpServers` component to utilize the new configuration structure. - Removed deprecated `agent_config.rs` and updated related imports. - Enhanced error handling for MCP server operations. - Updated frontend strategies to accommodate the new MCP configuration structure. feat: Introduce MCP configuration management - Added `McpConfig` struct for managing MCP server configurations. - Implemented reading and writing of agent config files in JSON and TOML formats. - Refactored MCP server handling in the `McpServers` component to utilize the new configuration structure. - Removed deprecated `agent_config.rs` and updated related imports. - Enhanced error handling for MCP server operations. - Updated frontend strategies to accommodate the new MCP configuration structure. Best effort migration; add missing feature flag feat: refactor execution process handling and introduce profile variant extraction feat: add default follow-up variant handling in task details context feat: enhance profile variant selection with dropdown menus in onboarding and task sections fmt, types * refactor: rename ProfileVariant to ProfileVariantLabel; Modified AgentProfile to wrap AgentProfileVariant Fmt, clippy * Fix rebase issues * refactor: replace OnceLock with RwLock for AgentProfiles caching; update profile retrieval in executors and routes --------- Co-authored-by: Gabriel Gordon-Hall <ggordonhall@gmail.com> Fmt Fix tests refactor: clean up unused imports and default implementations in executor modules Move profiles to profiles.rs * rename profile to profile_variant_label for readability rename AgentProfile to ProfileConfig, AgentProfileVariant to VariantAgentConfig * remove duplicated profile state * Amp yolo --------- Co-authored-by: Alex Netsch <alex@bloop.ai>
This commit is contained in:
committed by
GitHub
parent
2e07aa1a49
commit
9b4ca9dc45
@@ -10,15 +10,17 @@ pub enum ConfigError {
|
||||
Io(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
Json(#[from] serde_json::Error),
|
||||
#[error("Validation error: {0}")]
|
||||
ValidationError(String),
|
||||
}
|
||||
|
||||
pub type Config = versions::v3::Config;
|
||||
pub type NotificationConfig = versions::v3::NotificationConfig;
|
||||
pub type EditorConfig = versions::v3::EditorConfig;
|
||||
pub type ThemeMode = versions::v3::ThemeMode;
|
||||
pub type SoundFile = versions::v3::SoundFile;
|
||||
pub type EditorType = versions::v3::EditorType;
|
||||
pub type GitHubConfig = versions::v3::GitHubConfig;
|
||||
pub type Config = versions::v4::Config;
|
||||
pub type NotificationConfig = versions::v4::NotificationConfig;
|
||||
pub type EditorConfig = versions::v4::EditorConfig;
|
||||
pub type ThemeMode = versions::v4::ThemeMode;
|
||||
pub type SoundFile = versions::v4::SoundFile;
|
||||
pub type EditorType = versions::v4::EditorType;
|
||||
pub type GitHubConfig = versions::v4::GitHubConfig;
|
||||
|
||||
/// Will always return config, trying old schemas or eventually returning default
|
||||
pub async fn load_config_from_file(config_path: &PathBuf) -> Config {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub(super) mod v1;
|
||||
pub(super) mod v2;
|
||||
pub(super) mod v3;
|
||||
pub(super) mod v4;
|
||||
|
||||
110
crates/services/src/services/config/versions/v4.rs
Normal file
110
crates/services/src/services/config/versions/v4.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use anyhow::Error;
|
||||
use executors::profile::ProfileVariantLabel;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
pub use v3::{EditorConfig, EditorType, GitHubConfig, NotificationConfig, SoundFile, ThemeMode};
|
||||
|
||||
use crate::services::config::versions::v3;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, TS)]
|
||||
pub struct Config {
|
||||
pub config_version: String,
|
||||
pub theme: ThemeMode,
|
||||
pub profile: ProfileVariantLabel,
|
||||
pub disclaimer_acknowledged: bool,
|
||||
pub onboarding_acknowledged: bool,
|
||||
pub github_login_acknowledged: bool,
|
||||
pub telemetry_acknowledged: bool,
|
||||
pub notifications: NotificationConfig,
|
||||
pub editor: EditorConfig,
|
||||
pub github: GitHubConfig,
|
||||
pub analytics_enabled: Option<bool>,
|
||||
pub workspace_dir: Option<String>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_previous_version(raw_config: &str) -> Result<Self, Error> {
|
||||
let old_config = match serde_json::from_str::<v3::Config>(raw_config) {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => {
|
||||
tracing::error!("❌ Failed to parse config: {}", e);
|
||||
tracing::error!(" at line {}, column {}", e.line(), e.column());
|
||||
return Err(e.into());
|
||||
}
|
||||
};
|
||||
let mut onboarding_acknowledged = old_config.onboarding_acknowledged;
|
||||
let profile = match old_config.profile.as_str() {
|
||||
"claude-code" => ProfileVariantLabel::default("claude-code".to_string()),
|
||||
"claude-code-plan" => {
|
||||
ProfileVariantLabel::with_variant("claude-code".to_string(), "plan".to_string())
|
||||
}
|
||||
"claude-code-router" => {
|
||||
ProfileVariantLabel::with_variant("claude-code".to_string(), "router".to_string())
|
||||
}
|
||||
"amp" => ProfileVariantLabel::default("amp".to_string()),
|
||||
"gemini" => ProfileVariantLabel::default("gemini".to_string()),
|
||||
"codex" => ProfileVariantLabel::default("codex".to_string()),
|
||||
"opencode" => ProfileVariantLabel::default("opencode".to_string()),
|
||||
"qwen-code" => ProfileVariantLabel::default("qwen-code".to_string()),
|
||||
_ => {
|
||||
onboarding_acknowledged = false; // Reset the user's onboarding if executor is not supported
|
||||
ProfileVariantLabel::default("claude-code".to_string())
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
config_version: "v4".to_string(),
|
||||
theme: old_config.theme,
|
||||
profile,
|
||||
disclaimer_acknowledged: old_config.disclaimer_acknowledged,
|
||||
onboarding_acknowledged,
|
||||
github_login_acknowledged: old_config.github_login_acknowledged,
|
||||
telemetry_acknowledged: old_config.telemetry_acknowledged,
|
||||
notifications: old_config.notifications,
|
||||
editor: old_config.editor,
|
||||
github: old_config.github,
|
||||
analytics_enabled: old_config.analytics_enabled,
|
||||
workspace_dir: old_config.workspace_dir,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Config {
|
||||
fn from(raw_config: String) -> Self {
|
||||
if let Ok(config) = serde_json::from_str::<Config>(&raw_config)
|
||||
&& config.config_version == "v4"
|
||||
{
|
||||
return config;
|
||||
}
|
||||
|
||||
match Self::from_previous_version(&raw_config) {
|
||||
Ok(config) => {
|
||||
tracing::info!("Config upgraded to v3");
|
||||
config
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("Config migration failed: {}, using default", e);
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config_version: "v4".to_string(),
|
||||
theme: ThemeMode::System,
|
||||
profile: ProfileVariantLabel::default("claude-code".to_string()),
|
||||
disclaimer_acknowledged: false,
|
||||
onboarding_acknowledged: false,
|
||||
github_login_acknowledged: false,
|
||||
telemetry_acknowledged: false,
|
||||
notifications: NotificationConfig::default(),
|
||||
editor: EditorConfig::default(),
|
||||
github: GitHubConfig::default(),
|
||||
analytics_enabled: None,
|
||||
workspace_dir: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ use executors::{
|
||||
},
|
||||
executors::{CodingAgent, ExecutorError, StandardCodingAgentExecutor},
|
||||
logs::utils::patch::ConversationPatch,
|
||||
profile::ProfileVariantLabel,
|
||||
};
|
||||
use futures::{StreamExt, TryStreamExt, future};
|
||||
use sqlx::Error as SqlxError;
|
||||
@@ -309,22 +310,26 @@ pub trait ContainerService {
|
||||
// Spawn normalizer on populated store
|
||||
match executor_action.typ() {
|
||||
ExecutorActionType::CodingAgentInitialRequest(request) => {
|
||||
if let Ok(executor) = CodingAgent::from_profile_str(&request.profile) {
|
||||
if let Ok(executor) =
|
||||
CodingAgent::from_profile_variant_label(&request.profile_variant_label)
|
||||
{
|
||||
executor.normalize_logs(temp_store.clone(), ¤t_dir);
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Failed to resolve profile '{}' for normalization",
|
||||
request.profile
|
||||
"Failed to resolve profile '{:?}' for normalization",
|
||||
request.profile_variant_label
|
||||
);
|
||||
}
|
||||
}
|
||||
ExecutorActionType::CodingAgentFollowUpRequest(request) => {
|
||||
if let Ok(executor) = CodingAgent::from_profile_str(&request.profile) {
|
||||
if let Ok(executor) =
|
||||
CodingAgent::from_profile_variant_label(&request.profile_variant_label)
|
||||
{
|
||||
executor.normalize_logs(temp_store.clone(), ¤t_dir);
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Failed to resolve profile '{}' for normalization",
|
||||
request.profile
|
||||
"Failed to resolve profile '{:?}' for normalization",
|
||||
request.profile_variant_label
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -426,7 +431,7 @@ pub trait ContainerService {
|
||||
async fn start_attempt(
|
||||
&self,
|
||||
task_attempt: &TaskAttempt,
|
||||
profile_label: String,
|
||||
profile_variant_label: ProfileVariantLabel,
|
||||
) -> Result<ExecutionProcess, ContainerError> {
|
||||
// Create container
|
||||
self.create(task_attempt).await?;
|
||||
@@ -471,7 +476,7 @@ pub trait ContainerService {
|
||||
Some(Box::new(ExecutorAction::new(
|
||||
ExecutorActionType::CodingAgentInitialRequest(CodingAgentInitialRequest {
|
||||
prompt: task.to_prompt(),
|
||||
profile: profile_label,
|
||||
profile_variant_label,
|
||||
}),
|
||||
cleanup_action,
|
||||
))),
|
||||
@@ -487,7 +492,7 @@ pub trait ContainerService {
|
||||
let executor_action = ExecutorAction::new(
|
||||
ExecutorActionType::CodingAgentInitialRequest(CodingAgentInitialRequest {
|
||||
prompt: task.to_prompt(),
|
||||
profile: profile_label,
|
||||
profile_variant_label,
|
||||
}),
|
||||
cleanup_action,
|
||||
);
|
||||
@@ -529,13 +534,19 @@ pub trait ContainerService {
|
||||
ExecutionProcess::create(&self.db().pool, &create_execution_process, Uuid::new_v4())
|
||||
.await?;
|
||||
|
||||
if let ExecutorActionType::CodingAgentInitialRequest(coding_agent_request) =
|
||||
executor_action.typ()
|
||||
{
|
||||
if let Some(prompt) = match executor_action.typ() {
|
||||
ExecutorActionType::CodingAgentInitialRequest(coding_agent_request) => {
|
||||
Some(coding_agent_request.prompt.clone())
|
||||
}
|
||||
ExecutorActionType::CodingAgentFollowUpRequest(follow_up_request) => {
|
||||
Some(follow_up_request.prompt.clone())
|
||||
}
|
||||
_ => None,
|
||||
} {
|
||||
let create_executor_data = CreateExecutorSession {
|
||||
task_attempt_id: task_attempt.id,
|
||||
execution_process_id: execution_process.id,
|
||||
prompt: Some(coding_agent_request.prompt.clone()),
|
||||
prompt: Some(prompt),
|
||||
};
|
||||
|
||||
let executor_session_record_id = Uuid::new_v4();
|
||||
@@ -556,30 +567,34 @@ pub trait ContainerService {
|
||||
match executor_action.typ() {
|
||||
ExecutorActionType::CodingAgentInitialRequest(request) => {
|
||||
if let Some(msg_store) = self.get_msg_store_by_id(&execution_process.id).await {
|
||||
if let Ok(executor) = CodingAgent::from_profile_str(&request.profile) {
|
||||
if let Ok(executor) =
|
||||
CodingAgent::from_profile_variant_label(&request.profile_variant_label)
|
||||
{
|
||||
executor.normalize_logs(
|
||||
msg_store,
|
||||
&self.task_attempt_to_current_dir(task_attempt),
|
||||
);
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Failed to resolve profile '{}' for normalization",
|
||||
request.profile
|
||||
"Failed to resolve profile '{:?}' for normalization",
|
||||
request.profile_variant_label
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
ExecutorActionType::CodingAgentFollowUpRequest(request) => {
|
||||
if let Some(msg_store) = self.get_msg_store_by_id(&execution_process.id).await {
|
||||
if let Ok(executor) = CodingAgent::from_profile_str(&request.profile) {
|
||||
if let Ok(executor) =
|
||||
CodingAgent::from_profile_variant_label(&request.profile_variant_label)
|
||||
{
|
||||
executor.normalize_logs(
|
||||
msg_store,
|
||||
&self.task_attempt_to_current_dir(task_attempt),
|
||||
);
|
||||
} else {
|
||||
tracing::error!(
|
||||
"Failed to resolve profile '{}' for normalization",
|
||||
request.profile
|
||||
"Failed to resolve profile '{:?}' for normalization",
|
||||
request.profile_variant_label
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ fn build_gitignore_set(root: &Path) -> Result<Gitignore, FilesystemWatcherError>
|
||||
Ok(builder.build()?)
|
||||
}
|
||||
|
||||
fn path_allowed(path: &PathBuf, gi: &Gitignore, canonical_root: &Path) -> bool {
|
||||
fn path_allowed(path: &Path, gi: &Gitignore, canonical_root: &Path) -> bool {
|
||||
let canonical_path = canonicalize_lossy(path);
|
||||
|
||||
// Convert absolute path to relative path from the gitignore root
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use git2::{
|
||||
@@ -927,7 +927,7 @@ impl GitService {
|
||||
}
|
||||
|
||||
/// Get the default branch name for the repository
|
||||
pub fn get_default_branch_name(&self, repo_path: &PathBuf) -> Result<String, GitServiceError> {
|
||||
pub fn get_default_branch_name(&self, repo_path: &Path) -> Result<String, GitServiceError> {
|
||||
let repo = self.open_repo(repo_path)?;
|
||||
|
||||
match repo.head() {
|
||||
@@ -945,7 +945,7 @@ impl GitService {
|
||||
/// Extract GitHub owner and repo name from git repo path
|
||||
pub fn get_github_repo_info(
|
||||
&self,
|
||||
repo_path: &PathBuf,
|
||||
repo_path: &Path,
|
||||
) -> Result<(String, String), GitServiceError> {
|
||||
let repo = self.open_repo(repo_path)?;
|
||||
let remote = repo.find_remote("origin").map_err(|_| {
|
||||
|
||||
@@ -19,11 +19,11 @@ impl NotificationService {
|
||||
let message = match ctx.execution_process.status {
|
||||
ExecutionProcessStatus::Completed => format!(
|
||||
"✅ '{}' completed successfully\nBranch: {:?}\nExecutor: {}",
|
||||
ctx.task.title, ctx.task_attempt.branch, ctx.task_attempt.base_coding_agent
|
||||
ctx.task.title, ctx.task_attempt.branch, ctx.task_attempt.profile
|
||||
),
|
||||
ExecutionProcessStatus::Failed | ExecutionProcessStatus::Killed => format!(
|
||||
"❌ '{}' execution failed\nBranch: {:?}\nExecutor: {}",
|
||||
ctx.task.title, ctx.task_attempt.branch, ctx.task_attempt.base_coding_agent
|
||||
ctx.task.title, ctx.task_attempt.branch, ctx.task_attempt.profile
|
||||
),
|
||||
_ => {
|
||||
tracing::warn!(
|
||||
|
||||
Reference in New Issue
Block a user