Qwen-code (#430)

This commit is contained in:
Solomon
2025-08-12 10:35:19 +01:00
committed by GitHub
parent c704085408
commit 74db7161b6
8 changed files with 115 additions and 116 deletions

View File

@@ -62,6 +62,8 @@ pub struct AgentProfile {
pub agent: BaseCodingAgent,
/// Command builder configuration
pub command: CommandBuilder,
/// Optional profile-specific MCP config file path (absolute; supports leading ~). Overrides the default `BaseCodingAgent` config path
pub mcp_config_path: Option<String>,
}
impl AgentProfile {
@@ -75,6 +77,7 @@ impl AgentProfile {
"--verbose",
"--output-format=stream-json",
]),
mcp_config_path: None,
}
}
@@ -88,6 +91,7 @@ impl AgentProfile {
"--verbose",
"--output-format=stream-json",
]),
mcp_config_path: None,
}
}
@@ -103,6 +107,7 @@ impl AgentProfile {
"--output-format=stream-json",
],
),
mcp_config_path: None,
}
}
@@ -112,6 +117,7 @@ impl AgentProfile {
agent: BaseCodingAgent::Amp,
command: CommandBuilder::new("npx -y @sourcegraph/amp@0.0.1752148945-gd8844f")
.params(vec!["--format=jsonl"]),
mcp_config_path: None,
}
}
@@ -120,6 +126,7 @@ impl AgentProfile {
label: "gemini".to_string(),
agent: BaseCodingAgent::Gemini,
command: CommandBuilder::new("npx -y @google/gemini-cli@latest").params(vec!["--yolo"]),
mcp_config_path: None,
}
}
@@ -132,6 +139,17 @@ impl AgentProfile {
"--dangerously-bypass-approvals-and-sandbox",
"--skip-git-repo-check",
]),
mcp_config_path: None,
}
}
pub fn qwen_code() -> Self {
Self {
label: "qwen-code".to_string(),
agent: BaseCodingAgent::Gemini,
command: CommandBuilder::new("npx -y @qwen-code/qwen-code@latest")
.params(vec!["--yolo"]),
mcp_config_path: Some("~/.qwen/settings.json".to_string()),
}
}
@@ -141,6 +159,7 @@ impl AgentProfile {
agent: BaseCodingAgent::Opencode,
command: CommandBuilder::new("npx -y opencode-ai@latest run")
.params(vec!["--print-logs"]),
mcp_config_path: None,
}
}
}
@@ -179,6 +198,7 @@ impl AgentProfiles {
AgentProfile::gemini(),
AgentProfile::codex(),
AgentProfile::opencode(),
AgentProfile::qwen_code(),
],
}
}
@@ -240,98 +260,45 @@ mod tests {
use super::*;
#[test]
fn test_command_builder() {
let builder = CommandBuilder::new("npx claude").params(vec!["--verbose", "--json"]);
assert_eq!(builder.build_initial(), "npx claude --verbose --json");
assert_eq!(
builder.build_follow_up(&["--resume".to_string(), "session123".to_string()]),
"npx claude --verbose --json --resume session123"
);
}
fn default_profiles_have_expected_base_and_noninteractive_or_json_flags() {
// Build default profiles and make lookup by label easy
let profiles = AgentProfiles::from_defaults().to_map();
#[test]
fn test_default_profiles() {
let profiles = AgentProfiles::from_defaults();
assert!(profiles.profiles.len() == 7);
let get_profile_command = |label: &str| {
profiles
.get(label)
.map(|p| p.command.build_initial())
.unwrap_or_else(|| panic!("Profile not found: {label}"))
};
let claude_profile = profiles.get_profile("claude-code").unwrap();
assert_eq!(claude_profile.agent, BaseCodingAgent::ClaudeCode);
assert!(
claude_profile
.command
.build_initial()
.contains("claude-code")
);
assert!(
claude_profile
.command
.build_initial()
.contains("--dangerously-skip-permissions")
);
let claude_code_command = get_profile_command("claude-code");
assert!(claude_code_command.contains("npx -y @anthropic-ai/claude-code@latest"));
assert!(claude_code_command.contains("-p"));
assert!(claude_code_command.contains("--dangerously-skip-permissions"));
let amp_profile = profiles.get_profile("amp").unwrap();
assert_eq!(amp_profile.agent, BaseCodingAgent::Amp);
assert!(amp_profile.command.build_initial().contains("amp"));
assert!(
amp_profile
.command
.build_initial()
.contains("--format=jsonl")
);
let claude_code_router_command = get_profile_command("claude-code-router");
assert!(claude_code_router_command.contains("npx -y @musistudio/claude-code-router code"));
assert!(claude_code_router_command.contains("-p"));
assert!(claude_code_router_command.contains("--dangerously-skip-permissions"));
let gemini_profile = profiles.get_profile("gemini").unwrap();
assert_eq!(gemini_profile.agent, BaseCodingAgent::Gemini);
assert!(gemini_profile.command.build_initial().contains("gemini"));
assert!(gemini_profile.command.build_initial().contains("--yolo"));
let amp_command = get_profile_command("amp");
assert!(amp_command.contains("npx -y @sourcegraph/amp@0.0.1752148945-gd8844f"));
assert!(amp_command.contains("--format=jsonl"));
let codex_profile = profiles.get_profile("codex").unwrap();
assert_eq!(codex_profile.agent, BaseCodingAgent::Codex);
assert!(codex_profile.command.build_initial().contains("codex"));
assert!(codex_profile.command.build_initial().contains("--json"));
let gemini_command = get_profile_command("gemini");
assert!(gemini_command.contains("npx -y @google/gemini-cli@latest"));
assert!(gemini_command.contains("--yolo"));
let opencode_profile = profiles.get_profile("opencode").unwrap();
assert_eq!(opencode_profile.agent, BaseCodingAgent::Opencode);
assert!(
opencode_profile
.command
.build_initial()
.contains("opencode-ai")
);
assert!(opencode_profile.command.build_initial().contains("run"));
assert!(
opencode_profile
.command
.build_initial()
.contains("--print-logs")
);
let codex_command = get_profile_command("codex");
assert!(codex_command.contains("npx -y @openai/codex exec"));
assert!(codex_command.contains("--json"));
let claude_code_router_profile = profiles.get_profile("claude-code-router").unwrap();
assert_eq!(
claude_code_router_profile.agent,
BaseCodingAgent::ClaudeCode
);
assert!(
claude_code_router_profile
.command
.build_initial()
.contains("@musistudio/claude-code-router")
);
assert!(
claude_code_router_profile
.command
.build_initial()
.contains("--dangerously-skip-permissions")
);
}
let qwen_code_command = get_profile_command("qwen-code");
assert!(qwen_code_command.contains("npx -y @qwen-code/qwen-code@latest"));
assert!(qwen_code_command.contains("--yolo"));
#[test]
fn test_profiles_for_agent() {
let profiles = AgentProfiles::from_defaults();
let claude_profiles = profiles.get_profiles_for_agent(&BaseCodingAgent::ClaudeCode);
assert_eq!(claude_profiles.len(), 3); // default, plan mode, and claude-code-router
let amp_profiles = profiles.get_profiles_for_agent(&BaseCodingAgent::Amp);
assert_eq!(amp_profiles.len(), 1);
let opencode_command = get_profile_command("opencode");
assert!(opencode_command.contains("npx -y opencode-ai@latest run"));
assert!(opencode_command.contains("--print-logs"));
}
}

View File

@@ -46,6 +46,7 @@ git2 = "0.18"
mime_guess = "2.0"
rust-embed = "8.2"
octocrab = "0.44"
dirs = "5.0"
[dev-dependencies]
tempfile = "3.8"

View File

@@ -15,7 +15,7 @@ use serde_json::Value;
use services::services::config::{save_config_to_file, Config, SoundFile};
use tokio::fs;
use ts_rs::TS;
use utils::{assets::config_path, response::ApiResponse};
use utils::{assets::config_path, path::expand_tilde, response::ApiResponse};
use crate::{
error::ApiError,
@@ -115,6 +115,7 @@ async fn get_sound(Path(sound): Path<SoundFile>) -> Result<Response, ApiError> {
#[derive(Debug, Deserialize)]
struct McpServerQuery {
base_coding_agent: Option<BaseCodingAgent>,
mcp_config_path: Option<String>,
}
async fn get_mcp_servers(
@@ -138,13 +139,17 @@ async fn get_mcp_servers(
)));
}
// Get the config file path for this executor
let config_path = match agent.config_path() {
Some(path) => path,
None => {
return Ok(ResponseJson(ApiResponse::error(
"Could not determine config file path",
)));
// Resolve supplied config path or agent default
let config_path = if let Some(path_str) = &query.mcp_config_path {
expand_tilde(path_str)
} else {
match agent.config_path() {
Some(path) => path,
None => {
return Ok(ResponseJson(ApiResponse::error(
"Could not determine config file path",
)))
}
}
};
@@ -185,13 +190,17 @@ async fn update_mcp_servers(
)));
}
// Get the config file path for this executor
let config_path = match agent.config_path() {
Some(path) => path,
None => {
return Ok(ResponseJson(ApiResponse::error(
"Could not determine config file path",
)));
// Resolve supplied config path or agent default
let config_path = if let Some(path_str) = &query.mcp_config_path {
expand_tilde(path_str)
} else {
match agent.config_path() {
Some(path) => path,
None => {
return Ok(ResponseJson(ApiResponse::error(
"Could not determine config file path",
)))
}
}
};

View File

@@ -27,3 +27,4 @@ base64 = "0.22"
tokio = { workspace = true }
futures = "0.3.31"
tokio-stream = { version = "0.1.17", features = ["sync"] }
shellexpand = "3.1.1"

View File

@@ -88,6 +88,11 @@ pub fn get_vibe_kanban_temp_dir() -> std::path::PathBuf {
}
}
/// Expand leading ~ to user's home directory.
pub fn expand_tilde(path_str: &str) -> std::path::PathBuf {
shellexpand::tilde(path_str).as_ref().into()
}
#[cfg(test)]
mod tests {
use super::*;

View File

@@ -553,20 +553,25 @@ export const templatesApi = {
// MCP Servers APIs
export const mcpServersApi = {
load: async (executor: string): Promise<any> => {
const response = await makeRequest(
`/api/mcp-config?base_coding_agent=${encodeURIComponent(executor)}`
);
load: async (executor: string, mcpConfigPath?: string): Promise<any> => {
const params = new URLSearchParams();
params.set('base_coding_agent', executor);
if (mcpConfigPath) params.set('mcp_config_path', mcpConfigPath);
const response = await makeRequest(`/api/mcp-config?${params.toString()}`);
return handleApiResponse<any>(response);
},
save: async (executor: string, serversConfig: any): Promise<void> => {
const response = await makeRequest(
`/api/mcp-config?base_coding_agent=${encodeURIComponent(executor)}`,
{
method: 'POST',
body: JSON.stringify(serversConfig),
}
);
save: async (
executor: string,
mcpConfigPath: string | undefined,
serversConfig: any
): Promise<void> => {
const params = new URLSearchParams();
params.set('base_coding_agent', executor);
if (mcpConfigPath) params.set('mcp_config_path', mcpConfigPath);
const response = await makeRequest(`/api/mcp-config?${params.toString()}`, {
method: 'POST',
body: JSON.stringify(serversConfig),
});
if (!response.ok) {
const errorData = await response.json();
console.error('[API Error] Failed to save MCP servers', {

View File

@@ -63,8 +63,11 @@ export function McpServers() {
setMcpConfigPath('');
try {
// Load MCP servers for the selected profile's base agent
const result = await mcpServersApi.load(profile.agent);
// Load MCP servers for the selected profile/agent
const result = await mcpServersApi.load(
profile.agent,
profile.mcp_config_path || undefined
);
// Handle new response format with servers and config_path
const data = result || {};
const servers = data.servers || {};
@@ -160,7 +163,11 @@ export function McpServers() {
// Extract just the servers object for the API - backend will handle nesting/format
const mcpServersConfig = strategy.extractServersForApi(fullConfig);
await mcpServersApi.save(selectedProfile.agent, mcpServersConfig);
await mcpServersApi.save(
selectedProfile.agent,
mcpConfigPath || undefined,
mcpServersConfig
);
// Show success feedback
setSuccess(true);

View File

@@ -102,7 +102,11 @@ agent: BaseCodingAgent,
/**
* Command builder configuration
*/
command: CommandBuilder, };
command: CommandBuilder,
/**
* Optional profile-specific MCP config file path (absolute; supports leading ~). Overrides the default `BaseCodingAgent` config path
*/
mcp_config_path: string | null, };
export type AgentProfiles = { profiles: Array<AgentProfile>, };