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:
Gabriel Gordon-Hall
2025-08-14 17:33:33 +01:00
committed by GitHub
parent 2e07aa1a49
commit 9b4ca9dc45
64 changed files with 2086 additions and 1434 deletions

View File

@@ -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 {

View File

@@ -1,3 +1,4 @@
pub(super) mod v1;
pub(super) mod v2;
pub(super) mod v3;
pub(super) mod v4;

View 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,
}
}
}

View File

@@ -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(), &current_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(), &current_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
);
}
}

View File

@@ -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

View File

@@ -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(|_| {

View File

@@ -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!(