Skip to content

Commit a8afb47

Browse files
xianwwuXian Wubrandonskiser
authored
first round changes for agent generate for workshopping (#2690)
* adding in agent contribution metric * chore: fix lints * agent generate where user is prompted to include or exclude entire MCP servers * rebasing changes to newest api for conversation.rs * making changes according to Brandon's feedback * clippy * suppress warning messages * making changes after code review * felix feedback changes --------- Co-authored-by: Xian Wu <[email protected]> Co-authored-by: Brandon Kiser <[email protected]>
1 parent f0c5bd3 commit a8afb47

File tree

8 files changed

+522
-17
lines changed

8 files changed

+522
-17
lines changed

crates/chat-cli/src/cli/agent/mod.rs

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -208,17 +208,21 @@ impl Agent {
208208
/// This function mutates the agent to a state that is usable for runtime.
209209
/// Practically this means to convert some of the fields value to their usable counterpart.
210210
/// For example, converting the mcp array to actual mcp config and populate the agent file path.
211-
fn thaw(&mut self, path: &Path, legacy_mcp_config: Option<&McpServerConfig>) -> Result<(), AgentConfigError> {
211+
fn thaw(
212+
&mut self,
213+
path: &Path,
214+
legacy_mcp_config: Option<&McpServerConfig>,
215+
output: &mut impl Write,
216+
) -> Result<(), AgentConfigError> {
212217
let Self { mcp_servers, .. } = self;
213218

214219
self.path = Some(path.to_path_buf());
215220

216-
let mut stderr = std::io::stderr();
217221
if let (true, Some(legacy_mcp_config)) = (self.use_legacy_mcp_json, legacy_mcp_config) {
218222
for (name, legacy_server) in &legacy_mcp_config.mcp_servers {
219223
if mcp_servers.mcp_servers.contains_key(name) {
220224
let _ = queue!(
221-
stderr,
225+
output,
222226
style::SetForegroundColor(Color::Yellow),
223227
style::Print("WARNING: "),
224228
style::ResetColor,
@@ -238,7 +242,7 @@ impl Agent {
238242
}
239243
}
240244

241-
stderr.flush()?;
245+
output.flush()?;
242246

243247
Ok(())
244248
}
@@ -299,8 +303,8 @@ impl Agent {
299303
} else {
300304
None
301305
};
302-
303-
agent.thaw(&config_path, legacy_mcp_config.as_ref())?;
306+
let mut stderr = std::io::stderr();
307+
agent.thaw(&config_path, legacy_mcp_config.as_ref(), &mut stderr)?;
304308
Ok((agent, config_path))
305309
},
306310
_ => bail!("Agent {agent_name} does not exist"),
@@ -312,6 +316,7 @@ impl Agent {
312316
agent_path: impl AsRef<Path>,
313317
legacy_mcp_config: &mut Option<McpServerConfig>,
314318
mcp_enabled: bool,
319+
output: &mut impl Write,
315320
) -> Result<Agent, AgentConfigError> {
316321
let content = os.fs.read(&agent_path).await?;
317322
let mut agent = serde_json::from_slice::<Agent>(&content).map_err(|e| AgentConfigError::InvalidJson {
@@ -326,11 +331,11 @@ impl Agent {
326331
legacy_mcp_config.replace(config);
327332
}
328333
}
329-
agent.thaw(agent_path.as_ref(), legacy_mcp_config.as_ref())?;
334+
agent.thaw(agent_path.as_ref(), legacy_mcp_config.as_ref(), output)?;
330335
} else {
331336
agent.clear_mcp_configs();
332337
// Thaw the agent with empty MCP config to finalize normalization.
333-
agent.thaw(agent_path.as_ref(), None)?;
338+
agent.thaw(agent_path.as_ref(), None, output)?;
334339
}
335340
Ok(agent)
336341
}
@@ -495,7 +500,7 @@ impl Agents {
495500
};
496501

497502
let mut agents = Vec::<Agent>::new();
498-
let results = load_agents_from_entries(files, os, &mut global_mcp_config, mcp_enabled).await;
503+
let results = load_agents_from_entries(files, os, &mut global_mcp_config, mcp_enabled, output).await;
499504
for result in results {
500505
match result {
501506
Ok(agent) => agents.push(agent),
@@ -533,7 +538,7 @@ impl Agents {
533538
};
534539

535540
let mut agents = Vec::<Agent>::new();
536-
let results = load_agents_from_entries(files, os, &mut global_mcp_config, mcp_enabled).await;
541+
let results = load_agents_from_entries(files, os, &mut global_mcp_config, mcp_enabled, output).await;
537542
for result in results {
538543
match result {
539544
Ok(agent) => agents.push(agent),
@@ -834,6 +839,7 @@ async fn load_agents_from_entries(
834839
os: &Os,
835840
global_mcp_config: &mut Option<McpServerConfig>,
836841
mcp_enabled: bool,
842+
output: &mut impl Write,
837843
) -> Vec<Result<Agent, AgentConfigError>> {
838844
let mut res = Vec::<Result<Agent, AgentConfigError>>::new();
839845

@@ -844,7 +850,7 @@ async fn load_agents_from_entries(
844850
.and_then(OsStr::to_str)
845851
.is_some_and(|s| s == "json")
846852
{
847-
res.push(Agent::load(os, file_path, global_mcp_config, mcp_enabled).await);
853+
res.push(Agent::load(os, file_path, global_mcp_config, mcp_enabled, output).await);
848854
}
849855
}
850856

crates/chat-cli/src/cli/agent/root_command_args.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ impl AgentArgs {
140140
},
141141
Some(AgentSubcommands::Validate { path }) => {
142142
let mut global_mcp_config = None::<McpServerConfig>;
143-
let agent = Agent::load(os, path.as_str(), &mut global_mcp_config, mcp_enabled).await;
143+
let agent = Agent::load(os, path.as_str(), &mut global_mcp_config, mcp_enabled, &mut stderr).await;
144144

145145
'validate: {
146146
match agent {

crates/chat-cli/src/cli/chat/cli/editor.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ impl EditorArgs {
8383
}
8484

8585
/// Opens the user's preferred editor to compose a prompt
86-
fn open_editor(initial_text: Option<String>) -> Result<String, ChatError> {
86+
pub fn open_editor(initial_text: Option<String>) -> Result<String, ChatError> {
8787
// Create a temporary file with a unique name
8888
let temp_dir = std::env::temp_dir();
8989
let file_name = format!("q_prompt_{}.md", Uuid::new_v4());

crates/chat-cli/src/cli/chat/cli/profile.rs

Lines changed: 168 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::borrow::Cow;
2+
use std::collections::HashMap;
23
use std::io::Write;
34

45
use clap::Subcommand;
@@ -11,7 +12,11 @@ use crossterm::{
1112
execute,
1213
queue,
1314
};
14-
use dialoguer::Select;
15+
use dialoguer::{
16+
MultiSelect,
17+
Select,
18+
};
19+
use eyre::Result;
1520
use syntect::easy::HighlightLines;
1621
use syntect::highlighting::{
1722
Style,
@@ -26,8 +31,10 @@ use syntect::util::{
2631
use crate::cli::agent::{
2732
Agent,
2833
Agents,
34+
McpServerConfig,
2935
create_agent,
3036
};
37+
use crate::cli::chat::conversation::McpServerInfo;
3138
use crate::cli::chat::{
3239
ChatError,
3340
ChatSession,
@@ -36,6 +43,10 @@ use crate::cli::chat::{
3643
use crate::database::settings::Setting;
3744
use crate::os::Os;
3845
use crate::util::directories::chat_global_agent_path;
46+
use crate::util::{
47+
NullWriter,
48+
directories,
49+
};
3950

4051
#[deny(missing_docs)]
4152
#[derive(Debug, PartialEq, Subcommand)]
@@ -65,6 +76,8 @@ pub enum AgentSubcommand {
6576
#[arg(long, short)]
6677
from: Option<String>,
6778
},
79+
/// Generate an agent configuration using AI
80+
Generate {},
6881
/// Delete the specified agent
6982
#[command(hide = true)]
7083
Delete { name: String },
@@ -83,6 +96,22 @@ pub enum AgentSubcommand {
8396
Swap { name: Option<String> },
8497
}
8598

99+
fn prompt_mcp_server_selection(servers: &[McpServerInfo]) -> eyre::Result<Vec<&McpServerInfo>> {
100+
let items: Vec<String> = servers
101+
.iter()
102+
.map(|server| format!("{} ({})", server.name, server.config.command))
103+
.collect();
104+
105+
let selections = MultiSelect::new()
106+
.with_prompt("Select MCP servers (use Space to toggle, Enter to confirm)")
107+
.items(&items)
108+
.interact()?;
109+
110+
let selected_servers: Vec<&McpServerInfo> = selections.iter().filter_map(|&i| servers.get(i)).collect();
111+
112+
Ok(selected_servers)
113+
}
114+
86115
impl AgentSubcommand {
87116
pub async fn execute(self, os: &mut Os, session: &mut ChatSession) -> Result<ChatState, ChatError> {
88117
let agents = &session.conversation.agents;
@@ -146,8 +175,14 @@ impl AgentSubcommand {
146175
return Err(ChatError::Custom("Editor process did not exit with success".into()));
147176
}
148177

149-
let new_agent =
150-
Agent::load(os, &path_with_file_name, &mut None, session.conversation.mcp_enabled).await;
178+
let new_agent = Agent::load(
179+
os,
180+
&path_with_file_name,
181+
&mut None,
182+
session.conversation.mcp_enabled,
183+
&mut session.stderr,
184+
)
185+
.await;
151186
match new_agent {
152187
Ok(agent) => {
153188
session.conversation.agents.agents.insert(agent.name.clone(), agent);
@@ -184,6 +219,75 @@ impl AgentSubcommand {
184219
style::SetForegroundColor(Color::Reset)
185220
)?;
186221
},
222+
223+
Self::Generate {} => {
224+
let agent_name = match session.read_user_input("Enter agent name: ", false) {
225+
Some(input) => input.trim().to_string(),
226+
None => {
227+
return Ok(ChatState::PromptUser {
228+
skip_printing_tools: true,
229+
});
230+
},
231+
};
232+
233+
let agent_description = match session.read_user_input("Enter agent description: ", false) {
234+
Some(input) => input.trim().to_string(),
235+
None => {
236+
return Ok(ChatState::PromptUser {
237+
skip_printing_tools: true,
238+
});
239+
},
240+
};
241+
242+
let scope_options = vec!["Local (current workspace)", "Global (all workspaces)"];
243+
let scope_selection = Select::new()
244+
.with_prompt("Agent scope")
245+
.items(&scope_options)
246+
.default(0)
247+
.interact()
248+
.map_err(|e| ChatError::Custom(format!("Failed to get scope selection: {}", e).into()))?;
249+
250+
let is_global = scope_selection == 1;
251+
252+
let mcp_servers = get_enabled_mcp_servers(os)
253+
.await
254+
.map_err(|e| ChatError::Custom(e.to_string().into()))?;
255+
256+
let selected_servers = if mcp_servers.is_empty() {
257+
Vec::new()
258+
} else {
259+
prompt_mcp_server_selection(&mcp_servers).map_err(|e| ChatError::Custom(e.to_string().into()))?
260+
};
261+
262+
let mcp_servers_json = if !selected_servers.is_empty() {
263+
let servers: std::collections::HashMap<String, serde_json::Value> = selected_servers
264+
.iter()
265+
.map(|server| {
266+
(
267+
server.name.clone(),
268+
serde_json::to_value(&server.config).unwrap_or_default(),
269+
)
270+
})
271+
.collect();
272+
serde_json::to_string(&servers).unwrap_or_default()
273+
} else {
274+
"{}".to_string()
275+
};
276+
use schemars::schema_for;
277+
let schema = schema_for!(Agent);
278+
let schema_string = serde_json::to_string_pretty(&schema)
279+
.map_err(|e| ChatError::Custom(format!("Failed to serialize agent schema: {e}").into()))?;
280+
return session
281+
.generate_agent_config(
282+
os,
283+
&agent_name,
284+
&agent_description,
285+
&mcp_servers_json,
286+
&schema_string,
287+
is_global,
288+
)
289+
.await;
290+
},
187291
Self::Set { .. } | Self::Delete { .. } => {
188292
// As part of the agent implementation, we are disabling the ability to
189293
// switch / create profile after a session has started.
@@ -285,6 +389,7 @@ impl AgentSubcommand {
285389
match self {
286390
Self::List => "list",
287391
Self::Create { .. } => "create",
392+
Self::Generate { .. } => "generate",
288393
Self::Delete { .. } => "delete",
289394
Self::Set { .. } => "set",
290395
Self::Schema => "schema",
@@ -311,3 +416,63 @@ fn highlight_json(output: &mut impl Write, json_str: &str) -> eyre::Result<()> {
311416

312417
Ok(execute!(output, style::ResetColor)?)
313418
}
419+
420+
/// Searches all configuration sources for MCP servers and returns a deduplicated list.
421+
/// Priority order: Agent configs > Workspace legacy > Global legacy
422+
pub async fn get_all_available_mcp_servers(os: &mut Os) -> Result<Vec<McpServerInfo>> {
423+
let mut servers = HashMap::<String, McpServerInfo>::new();
424+
425+
// 1. Load from agent configurations (highest priority)
426+
let mut null_writer = NullWriter;
427+
let (agents, _) = Agents::load(os, None, true, &mut null_writer, true).await;
428+
429+
for (_, agent) in agents.agents {
430+
for (server_name, server_config) in agent.mcp_servers.mcp_servers {
431+
if !servers.values().any(|s| s.config.command == server_config.command) {
432+
servers.insert(server_name.clone(), McpServerInfo {
433+
name: server_name,
434+
config: server_config,
435+
});
436+
}
437+
}
438+
}
439+
440+
// 2. Load from workspace legacy config (medium priority)
441+
if let Ok(workspace_path) = directories::chat_legacy_workspace_mcp_config(os) {
442+
if let Ok(workspace_config) = McpServerConfig::load_from_file(os, workspace_path).await {
443+
for (server_name, server_config) in workspace_config.mcp_servers {
444+
if !servers.values().any(|s| s.config.command == server_config.command) {
445+
servers.insert(server_name.clone(), McpServerInfo {
446+
name: server_name,
447+
config: server_config,
448+
});
449+
}
450+
}
451+
}
452+
}
453+
454+
// 3. Load from global legacy config (lowest priority)
455+
if let Ok(global_path) = directories::chat_legacy_global_mcp_config(os) {
456+
if let Ok(global_config) = McpServerConfig::load_from_file(os, global_path).await {
457+
for (server_name, server_config) in global_config.mcp_servers {
458+
if !servers.values().any(|s| s.config.command == server_config.command) {
459+
servers.insert(server_name.clone(), McpServerInfo {
460+
name: server_name,
461+
config: server_config,
462+
});
463+
}
464+
}
465+
}
466+
}
467+
468+
Ok(servers.into_values().collect())
469+
}
470+
471+
/// Get only enabled MCP servers (excludes disabled ones)
472+
pub async fn get_enabled_mcp_servers(os: &mut Os) -> Result<Vec<McpServerInfo>> {
473+
let all_servers = get_all_available_mcp_servers(os).await?;
474+
Ok(all_servers
475+
.into_iter()
476+
.filter(|server| !server.config.disabled)
477+
.collect())
478+
}

0 commit comments

Comments
 (0)