mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(tui/agents): interactive /agents UI with list + editor; remove param-based flow\n\n- New bottom pane views: AgentsSettingsView (list) and SubagentEditorView (form)\n- Shows built-in /plan, /solve, /code plus custom and Add New…\n- Editor provides Name, Mode (read-only/write), Agents multi-select summary,\n Orchestrator and Agent instruction fields, with [ Save ] [ Cancel ]\n- Persist via config_edit::upsert_subagent_command and update in-memory config\n- AppEvent::ShowSubagentEditor to open editor from list\n- Remove old /agents param parsing; keep zero warnings build
This commit is contained in:
@@ -1027,6 +1027,16 @@ impl App<'_> {
|
|||||||
widget.toggle_mcp_server(&name, enable);
|
widget.toggle_mcp_server(&name, enable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AppEvent::UpdateSubagentCommand(cmd) => {
|
||||||
|
if let AppState::Chat { widget } = &mut self.app_state {
|
||||||
|
widget.apply_subagent_update(cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppEvent::ShowSubagentEditor { name, available_agents, existing, is_new } => {
|
||||||
|
if let AppState::Chat { widget } = &mut self.app_state {
|
||||||
|
widget.show_subagent_editor_ui(name, available_agents, existing, is_new);
|
||||||
|
}
|
||||||
|
}
|
||||||
AppEvent::PrefillComposer(text) => {
|
AppEvent::PrefillComposer(text) => {
|
||||||
if let AppState::Chat { widget } = &mut self.app_state {
|
if let AppState::Chat { widget } = &mut self.app_state {
|
||||||
widget.insert_str(&text);
|
widget.insert_str(&text);
|
||||||
|
|||||||
@@ -88,6 +88,15 @@ pub(crate) enum AppEvent {
|
|||||||
|
|
||||||
/// Update the theme (with history event)
|
/// Update the theme (with history event)
|
||||||
UpdateTheme(ThemeName),
|
UpdateTheme(ThemeName),
|
||||||
|
/// Add or update a subagent command in memory (UI already persisted to config.toml)
|
||||||
|
UpdateSubagentCommand(codex_core::config_types::SubagentCommandConfig),
|
||||||
|
/// Open the subagent editor UI for the given command name
|
||||||
|
ShowSubagentEditor {
|
||||||
|
name: String,
|
||||||
|
available_agents: Vec<String>,
|
||||||
|
existing: Vec<codex_core::config_types::SubagentCommandConfig>,
|
||||||
|
is_new: bool,
|
||||||
|
},
|
||||||
|
|
||||||
/// Preview theme (no history event)
|
/// Preview theme (no history event)
|
||||||
PreviewTheme(ThemeName),
|
PreviewTheme(ThemeName),
|
||||||
|
|||||||
@@ -0,0 +1,242 @@
|
|||||||
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
use ratatui::buffer::Buffer;
|
||||||
|
use ratatui::layout::{Alignment, Rect};
|
||||||
|
use ratatui::style::{Modifier, Style};
|
||||||
|
use ratatui::text::{Line, Span};
|
||||||
|
use ratatui::widgets::{Block, Borders, Clear, Paragraph, Widget};
|
||||||
|
|
||||||
|
use crate::app_event::AppEvent;
|
||||||
|
use crate::app_event_sender::AppEventSender;
|
||||||
|
|
||||||
|
use super::bottom_pane_view::BottomPaneView;
|
||||||
|
use super::list_selection_view::{ListSelectionView, SelectionAction, SelectionItem};
|
||||||
|
use super::BottomPane;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AgentsSettingsView {
|
||||||
|
builtins: Vec<String>,
|
||||||
|
custom: Vec<String>,
|
||||||
|
existing: Vec<codex_core::config_types::SubagentCommandConfig>,
|
||||||
|
available_agents: Vec<String>,
|
||||||
|
app_event_tx: AppEventSender,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentsSettingsView {
|
||||||
|
pub fn new(
|
||||||
|
builtins: Vec<String>,
|
||||||
|
custom: Vec<String>,
|
||||||
|
existing: Vec<codex_core::config_types::SubagentCommandConfig>,
|
||||||
|
available_agents: Vec<String>,
|
||||||
|
app_event_tx: AppEventSender,
|
||||||
|
) -> Self {
|
||||||
|
Self { builtins, custom, existing, available_agents, app_event_tx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_list_view(self) -> ListSelectionView {
|
||||||
|
// Build items: built-ins first, then custom, then Add New…
|
||||||
|
let mut items: Vec<SelectionItem> = Vec::new();
|
||||||
|
let make_actions = |name: String| -> Vec<SelectionAction> {
|
||||||
|
let name_clone = name.clone();
|
||||||
|
let view_clone = self.clone();
|
||||||
|
vec![Box::new(move |tx: &AppEventSender| {
|
||||||
|
tx.send(AppEvent::ShowSubagentEditor { name: name_clone.clone(), available_agents: view_clone.available_agents.clone(), existing: view_clone.existing.clone(), is_new: false });
|
||||||
|
})]
|
||||||
|
};
|
||||||
|
|
||||||
|
// Built-ins
|
||||||
|
for name in &self.builtins {
|
||||||
|
let desc = Some("(press Enter to configure)".to_string());
|
||||||
|
items.push(SelectionItem { name: format!("/{}", name), description: desc, is_current: false, actions: make_actions(name.clone()) });
|
||||||
|
}
|
||||||
|
// Custom
|
||||||
|
for name in &self.custom {
|
||||||
|
let desc = Some("(press Enter to configure)".to_string());
|
||||||
|
items.push(SelectionItem { name: format!("/{}", name), description: desc, is_current: false, actions: make_actions(name.clone()) });
|
||||||
|
}
|
||||||
|
// Add New…
|
||||||
|
{
|
||||||
|
let view_clone = self.clone();
|
||||||
|
let actions: Vec<SelectionAction> = vec![Box::new(move |tx: &AppEventSender| {
|
||||||
|
tx.send(AppEvent::ShowSubagentEditor { name: String::new(), available_agents: view_clone.available_agents.clone(), existing: view_clone.existing.clone(), is_new: true });
|
||||||
|
})];
|
||||||
|
items.push(SelectionItem { name: "Add new…".to_string(), description: None, is_current: false, actions });
|
||||||
|
}
|
||||||
|
|
||||||
|
ListSelectionView::new(
|
||||||
|
" Agent Commands ".to_string(),
|
||||||
|
Some("Press Enter to configure".to_string()),
|
||||||
|
Some("Esc cancel".to_string()),
|
||||||
|
items,
|
||||||
|
self.app_event_tx.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SubagentEditorView {
|
||||||
|
name: String,
|
||||||
|
read_only: bool,
|
||||||
|
selected_agent_indices: Vec<usize>,
|
||||||
|
orchestrator: String,
|
||||||
|
agent: String,
|
||||||
|
available_agents: Vec<String>,
|
||||||
|
is_new: bool,
|
||||||
|
field: usize, // 0 name, 1 mode, 2 agents, 3 orch, 4 agent, 5 save, 6 cancel
|
||||||
|
app_event_tx: AppEventSender,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubagentEditorView {
|
||||||
|
pub fn new(root: &AgentsSettingsView, name: &str) -> Self {
|
||||||
|
let mut me = Self {
|
||||||
|
name: name.to_string(),
|
||||||
|
read_only: matches!(name, "plan" | "solve"),
|
||||||
|
selected_agent_indices: Vec::new(),
|
||||||
|
orchestrator: String::new(),
|
||||||
|
agent: String::new(),
|
||||||
|
available_agents: root.available_agents.clone(),
|
||||||
|
is_new: name.is_empty(),
|
||||||
|
field: 0,
|
||||||
|
app_event_tx: root.app_event_tx.clone(),
|
||||||
|
};
|
||||||
|
// Seed from existing config if present
|
||||||
|
if let Some(cfg) = root.existing.iter().find(|c| c.name.eq_ignore_ascii_case(name)) {
|
||||||
|
me.name = cfg.name.clone();
|
||||||
|
me.read_only = cfg.read_only;
|
||||||
|
me.orchestrator = cfg.orchestrator_instructions.clone().unwrap_or_default();
|
||||||
|
me.agent = cfg.agent_instructions.clone().unwrap_or_default();
|
||||||
|
let set: std::collections::HashSet<String> = cfg.agents.iter().cloned().collect();
|
||||||
|
for (idx, a) in me.available_agents.iter().enumerate() {
|
||||||
|
if set.contains(a) { me.selected_agent_indices.push(idx); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
me
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_data(
|
||||||
|
name: String,
|
||||||
|
available_agents: Vec<String>,
|
||||||
|
existing: Vec<codex_core::config_types::SubagentCommandConfig>,
|
||||||
|
is_new: bool,
|
||||||
|
app_event_tx: AppEventSender,
|
||||||
|
) -> Self {
|
||||||
|
let root = AgentsSettingsView { builtins: vec![], custom: vec![], existing, available_agents, app_event_tx };
|
||||||
|
let mut s = Self::new(&root, &name);
|
||||||
|
s.is_new = is_new;
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_agent_at(&mut self, idx: usize) {
|
||||||
|
if let Some(pos) = self.selected_agent_indices.iter().position(|i| *i == idx) {
|
||||||
|
self.selected_agent_indices.remove(pos);
|
||||||
|
} else {
|
||||||
|
self.selected_agent_indices.push(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(&mut self) {
|
||||||
|
let agents: Vec<String> = if self.selected_agent_indices.is_empty() {
|
||||||
|
Vec::new()
|
||||||
|
} else {
|
||||||
|
self.selected_agent_indices.iter().filter_map(|i| self.available_agents.get(*i).cloned()).collect()
|
||||||
|
};
|
||||||
|
let cfg = codex_core::config_types::SubagentCommandConfig {
|
||||||
|
name: self.name.clone(),
|
||||||
|
read_only: self.read_only,
|
||||||
|
agents,
|
||||||
|
orchestrator_instructions: if self.orchestrator.trim().is_empty() { None } else { Some(self.orchestrator.clone()) },
|
||||||
|
agent_instructions: if self.agent.trim().is_empty() { None } else { Some(self.agent.clone()) },
|
||||||
|
};
|
||||||
|
// Persist to disk
|
||||||
|
if let Ok(home) = codex_core::config::find_codex_home() {
|
||||||
|
let rt = tokio::runtime::Handle::current();
|
||||||
|
let _ = rt.block_on(codex_core::config_edit::upsert_subagent_command(&home, &cfg));
|
||||||
|
}
|
||||||
|
// Update in-memory config
|
||||||
|
self.app_event_tx.send(AppEvent::UpdateSubagentCommand(cfg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> BottomPaneView<'a> for SubagentEditorView {
|
||||||
|
fn handle_key_event(&mut self, _pane: &mut BottomPane<'a>, key_event: KeyEvent) {
|
||||||
|
match key_event {
|
||||||
|
KeyEvent { code: KeyCode::Esc, .. } => { /* close handled by parent */ }
|
||||||
|
KeyEvent { code: KeyCode::Tab, .. } => { self.field = (self.field + 1).min(6); }
|
||||||
|
KeyEvent { code: KeyCode::BackTab, .. } => { if self.field > 0 { self.field -= 1; } }
|
||||||
|
KeyEvent { code: KeyCode::Left | KeyCode::Right, .. } if self.field == 1 => { self.read_only = !self.read_only; }
|
||||||
|
KeyEvent { code: KeyCode::Char(' '), .. } if self.field == 2 => {
|
||||||
|
// toggle currently highlighted agent (first one for simplicity)
|
||||||
|
self.toggle_agent_at(0);
|
||||||
|
}
|
||||||
|
KeyEvent { code: KeyCode::Enter, .. } if self.field == 5 => { self.save(); }
|
||||||
|
KeyEvent { code: KeyCode::Char(c), modifiers: KeyModifiers::NONE, .. } => {
|
||||||
|
match self.field {
|
||||||
|
0 => self.name.push(c),
|
||||||
|
3 => self.orchestrator.push(c),
|
||||||
|
4 => self.agent.push(c),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyEvent { code: KeyCode::Backspace, .. } => {
|
||||||
|
match self.field {
|
||||||
|
0 => { self.name.pop(); },
|
||||||
|
3 => { self.orchestrator.pop(); },
|
||||||
|
4 => { self.agent.pop(); },
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_complete(&self) -> bool { false }
|
||||||
|
|
||||||
|
fn desired_height(&self, _width: u16) -> u16 { 20 }
|
||||||
|
|
||||||
|
fn render(&self, area: Rect, buf: &mut Buffer) {
|
||||||
|
Clear.render(area, buf);
|
||||||
|
let block = Block::default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(crate::colors::border()))
|
||||||
|
.style(Style::default().bg(crate::colors::background()).fg(crate::colors::text()))
|
||||||
|
.title(" Configure Agent Command ")
|
||||||
|
.title_alignment(Alignment::Center);
|
||||||
|
let inner = block.inner(area);
|
||||||
|
block.render(area, buf);
|
||||||
|
|
||||||
|
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||||
|
let sel = |idx: usize| if self.field == idx { Style::default().bg(crate::colors::selection()).add_modifier(Modifier::BOLD) } else { Style::default() };
|
||||||
|
|
||||||
|
lines.push(Line::from(vec![Span::styled("Name: ", Style::default()), Span::styled(self.name.clone(), sel(0))]));
|
||||||
|
let mode_str = if self.read_only { "read-only" } else { "write" };
|
||||||
|
lines.push(Line::from(vec![Span::styled("Mode: ", Style::default()), Span::styled(mode_str.to_string(), sel(1))]));
|
||||||
|
|
||||||
|
// Agents selection (simple summary; toggle behavior simplified)
|
||||||
|
let mut sel_names: Vec<String> = Vec::new();
|
||||||
|
for (idx, a) in self.available_agents.iter().enumerate() {
|
||||||
|
let checked = if self.selected_agent_indices.contains(&idx) { "[x]" } else { "[ ]" };
|
||||||
|
sel_names.push(format!("{} {}", checked, a));
|
||||||
|
}
|
||||||
|
lines.push(Line::from("Agents:"));
|
||||||
|
lines.push(Line::from(vec![Span::styled(sel_names.join(", "), sel(2))]));
|
||||||
|
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
lines.push(Line::from("Instructions to Code (orchestrator):"));
|
||||||
|
lines.push(Line::from(Span::styled(self.orchestrator.clone(), sel(3))));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
lines.push(Line::from("Instructions to each agent:"));
|
||||||
|
lines.push(Line::from(Span::styled(self.agent.clone(), sel(4))));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
|
||||||
|
let save_style = sel(5).fg(crate::colors::success());
|
||||||
|
let cancel_style = sel(6).fg(crate::colors::error());
|
||||||
|
lines.push(Line::from(vec![
|
||||||
|
Span::styled("[ Save ] ", save_style),
|
||||||
|
Span::styled("[ Cancel ]", cancel_style),
|
||||||
|
]));
|
||||||
|
|
||||||
|
let paragraph = Paragraph::new(lines)
|
||||||
|
.alignment(Alignment::Left)
|
||||||
|
.style(Style::default().bg(crate::colors::background()).fg(crate::colors::text()));
|
||||||
|
paragraph.render(Rect { x: inner.x.saturating_add(1), y: inner.y, width: inner.width.saturating_sub(2), height: inner.height }, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ mod scroll_state;
|
|||||||
mod selection_popup_common;
|
mod selection_popup_common;
|
||||||
pub mod list_selection_view;
|
pub mod list_selection_view;
|
||||||
pub mod resume_selection_view;
|
pub mod resume_selection_view;
|
||||||
|
pub mod agents_settings_view;
|
||||||
mod github_settings_view;
|
mod github_settings_view;
|
||||||
pub mod mcp_settings_view;
|
pub mod mcp_settings_view;
|
||||||
// no direct use of list_selection_view or its items here
|
// no direct use of list_selection_view or its items here
|
||||||
@@ -497,6 +498,21 @@ impl BottomPane<'_> {
|
|||||||
self.request_redraw();
|
self.request_redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Show Subagent editor UI
|
||||||
|
pub fn show_subagent_editor(
|
||||||
|
&mut self,
|
||||||
|
name: String,
|
||||||
|
available_agents: Vec<String>,
|
||||||
|
existing: Vec<codex_core::config_types::SubagentCommandConfig>,
|
||||||
|
is_new: bool,
|
||||||
|
) {
|
||||||
|
use crate::bottom_pane::agents_settings_view::SubagentEditorView;
|
||||||
|
let view = SubagentEditorView::new_with_data(name, available_agents, existing, is_new, self.app_event_tx.clone());
|
||||||
|
self.active_view = Some(Box::new(view));
|
||||||
|
self.status_view_active = false;
|
||||||
|
self.request_redraw();
|
||||||
|
}
|
||||||
|
|
||||||
/// Height (terminal rows) required by the current bottom pane.
|
/// Height (terminal rows) required by the current bottom pane.
|
||||||
pub(crate) fn request_redraw(&self) {
|
pub(crate) fn request_redraw(&self) {
|
||||||
self.app_event_tx.send(AppEvent::RequestRedraw)
|
self.app_event_tx.send(AppEvent::RequestRedraw)
|
||||||
|
|||||||
+56
-146
@@ -4144,6 +4144,7 @@ impl ChatWidget<'_> {
|
|||||||
self.history_push(history_cell::new_prompts_output());
|
self.history_push(history_cell::new_prompts_output());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) fn add_agents_output(&mut self) {
|
pub(crate) fn add_agents_output(&mut self) {
|
||||||
use ratatui::text::Line;
|
use ratatui::text::Line;
|
||||||
|
|
||||||
@@ -4299,158 +4300,67 @@ impl ChatWidget<'_> {
|
|||||||
self.request_redraw();
|
self.request_redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn handle_agents_command(&mut self, args: String) {
|
pub(crate) fn handle_agents_command(&mut self, _args: String) {
|
||||||
let trimmed = args.trim();
|
// Open interactive Agents settings view (list + per-command editor)
|
||||||
if trimmed.is_empty() {
|
self.show_agents_settings_ui();
|
||||||
self.add_agents_output();
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_kv(args: &str) -> std::collections::HashMap<String, String> {
|
fn show_agents_settings_ui(&mut self) {
|
||||||
let mut map = std::collections::HashMap::new();
|
use crate::bottom_pane::agents_settings_view::AgentsSettingsView;
|
||||||
for tok in args.split_whitespace() {
|
let available_agents: Vec<String> = if self.config.agents.is_empty() {
|
||||||
if let Some((k, v)) = tok.split_once('=') {
|
vec!["claude".into(), "gemini".into(), "qwen".into(), "codex".into()]
|
||||||
map.insert(k.to_string(), v.trim_matches('"').to_string());
|
} else {
|
||||||
}
|
self.config
|
||||||
}
|
.agents
|
||||||
map
|
.iter()
|
||||||
}
|
.filter(|a| a.enabled)
|
||||||
|
.map(|a| a.name.clone())
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
let builtins = vec!["plan".to_string(), "solve".to_string(), "code".to_string()];
|
||||||
|
let custom: Vec<String> = self
|
||||||
|
.config
|
||||||
|
.subagent_commands
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.name.clone())
|
||||||
|
.filter(|n| !builtins.iter().any(|b| b.eq_ignore_ascii_case(n)))
|
||||||
|
.collect();
|
||||||
|
let view = AgentsSettingsView::new(
|
||||||
|
builtins,
|
||||||
|
custom,
|
||||||
|
self.config.subagent_commands.clone(),
|
||||||
|
available_agents,
|
||||||
|
self.app_event_tx.clone(),
|
||||||
|
);
|
||||||
|
self.bottom_pane.show_list_selection(" Agents ".to_string(), None, None, view.into_list_view());
|
||||||
|
}
|
||||||
|
|
||||||
let mut parts = trimmed.splitn(2, ' ');
|
pub(crate) fn show_subagent_editor_ui(
|
||||||
let action = parts.next().unwrap_or("");
|
&mut self,
|
||||||
let rest = parts.next().unwrap_or("");
|
name: String,
|
||||||
let kv = parse_kv(rest);
|
available_agents: Vec<String>,
|
||||||
|
existing: Vec<codex_core::config_types::SubagentCommandConfig>,
|
||||||
|
is_new: bool,
|
||||||
|
) {
|
||||||
|
self.bottom_pane
|
||||||
|
.show_subagent_editor(name, available_agents, existing, is_new);
|
||||||
|
}
|
||||||
|
|
||||||
match action {
|
pub(crate) fn apply_subagent_update(&mut self, cmd: codex_core::config_types::SubagentCommandConfig) {
|
||||||
"add" | "edit" => {
|
if let Some(slot) = self
|
||||||
let name = kv.get("name").cloned().unwrap_or_default();
|
.config
|
||||||
if name.is_empty() {
|
.subagent_commands
|
||||||
self.history_push(crate::history_cell::new_error_event(
|
.iter_mut()
|
||||||
"usage: /agents add name=<name> ...".to_string(),
|
.find(|c| c.name.eq_ignore_ascii_case(&cmd.name))
|
||||||
));
|
{
|
||||||
return;
|
*slot = cmd;
|
||||||
}
|
} else {
|
||||||
|
self.config.subagent_commands.push(cmd);
|
||||||
let mut existing = self
|
|
||||||
.config
|
|
||||||
.subagent_commands
|
|
||||||
.iter()
|
|
||||||
.find(|c| c.name.eq_ignore_ascii_case(&name))
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default();
|
|
||||||
if existing.name.is_empty() {
|
|
||||||
existing.name = name.clone();
|
|
||||||
// default read-only for built-in names if not set during add
|
|
||||||
if existing.read_only == false && (name.eq_ignore_ascii_case("plan") || name.eq_ignore_ascii_case("solve")) {
|
|
||||||
existing.read_only = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = kv.get("read-only") {
|
|
||||||
existing.read_only = matches!(v.as_str(), "true" | "1" | "yes");
|
|
||||||
}
|
|
||||||
if let Some(v) = kv.get("agents") {
|
|
||||||
let list = v
|
|
||||||
.split(',')
|
|
||||||
.map(|s| s.trim().to_string())
|
|
||||||
.filter(|s| !s.is_empty())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
existing.agents = list;
|
|
||||||
}
|
|
||||||
if let Some(v) = kv.get("orchestrator").or_else(|| kv.get("orchestrator-instructions")) {
|
|
||||||
existing.orchestrator_instructions = Some(v.clone());
|
|
||||||
}
|
|
||||||
if let Some(v) = kv.get("agent").or_else(|| kv.get("agent-instructions")) {
|
|
||||||
existing.agent_instructions = Some(v.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let codex_home = match codex_core::config::find_codex_home() {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
self.history_push(crate::history_cell::new_error_event(format!(
|
|
||||||
"failed to locate CODEX_HOME: {}",
|
|
||||||
e
|
|
||||||
)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let fut = codex_core::config_edit::upsert_subagent_command(&codex_home, &existing);
|
|
||||||
let rt = tokio::runtime::Handle::current();
|
|
||||||
let res = rt.block_on(fut);
|
|
||||||
match res {
|
|
||||||
Ok(()) => {
|
|
||||||
if let Some(slot) = self
|
|
||||||
.config
|
|
||||||
.subagent_commands
|
|
||||||
.iter_mut()
|
|
||||||
.find(|c| c.name.eq_ignore_ascii_case(&existing.name))
|
|
||||||
{
|
|
||||||
*slot = existing.clone();
|
|
||||||
} else {
|
|
||||||
self.config.subagent_commands.push(existing.clone());
|
|
||||||
}
|
|
||||||
self.history_push(crate::history_cell::PlainHistoryCell {
|
|
||||||
lines: vec![ratatui::text::Line::from(format!(
|
|
||||||
"✅ saved subagent command '{}'",
|
|
||||||
name
|
|
||||||
))],
|
|
||||||
kind: crate::history_cell::HistoryCellType::Notice,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
self.history_push(crate::history_cell::new_error_event(format!(
|
|
||||||
"failed to save subagent command: {}",
|
|
||||||
e
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"delete" => {
|
|
||||||
let name = kv.get("name").cloned().unwrap_or_default();
|
|
||||||
if name.is_empty() {
|
|
||||||
self.history_push(crate::history_cell::new_error_event(
|
|
||||||
"usage: /agents delete name=<name>".to_string(),
|
|
||||||
));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let codex_home = match codex_core::config::find_codex_home() {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
self.history_push(crate::history_cell::new_error_event(format!("failed to locate CODEX_HOME: {}", e)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let fut = codex_core::config_edit::delete_subagent_command(&codex_home, &name);
|
|
||||||
let rt = tokio::runtime::Handle::current();
|
|
||||||
let res = rt.block_on(fut);
|
|
||||||
match res {
|
|
||||||
Ok(true) => {
|
|
||||||
self.config
|
|
||||||
.subagent_commands
|
|
||||||
.retain(|c| !c.name.eq_ignore_ascii_case(&name));
|
|
||||||
self.history_push(crate::history_cell::PlainHistoryCell {
|
|
||||||
lines: vec![ratatui::text::Line::from(format!("🗑 removed '{}'", name))],
|
|
||||||
kind: crate::history_cell::HistoryCellType::Notice,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Ok(false) => self.history_push(crate::history_cell::new_error_event(format!(
|
|
||||||
"no subagent command named '{}'",
|
|
||||||
name
|
|
||||||
))),
|
|
||||||
Err(e) => self.history_push(crate::history_cell::new_error_event(format!(
|
|
||||||
"failed to delete subagent command: {}",
|
|
||||||
e
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.history_push(crate::history_cell::new_error_event(
|
|
||||||
"usage: /agents [add|edit|delete] ...".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub(crate) fn show_diffs_popup(&mut self) {
|
pub(crate) fn show_diffs_popup(&mut self) {
|
||||||
use crate::diff_render::create_diff_details_only;
|
use crate::diff_render::create_diff_details_only;
|
||||||
// Build a latest-first unique file list
|
// Build a latest-first unique file list
|
||||||
|
|||||||
Reference in New Issue
Block a user