feat(tui/agents): simplify to single orchestrator input, restore labels, and prefill fields\n\n- Remove per-agent instructions field; keep a single parent (orchestrator) text field that resizes with content.\n- Prefill Name for built-ins and existing commands; retain selected agents when editing.\n- Restore and preserve titles/labels by reserving dynamic blank rows under the label and rendering the field there (no overlap).\n- Adjust navigation: 0 Name, 1 Mode, 2 Agents, 3 Orchestrator, 4 Save, 5 Cancel.\n- Update desired height to include orchestrator field rows; drop agent rows.\n- Persist only orchestrator_instructions; always clear agent_instructions.

This commit is contained in:
James Peter
2025-09-15 10:31:04 +10:00
parent 891fb45007
commit 647fed3688
@@ -84,10 +84,9 @@ pub struct SubagentEditorView {
selected_agent_indices: Vec<usize>,
agent_cursor: usize,
orch_field: FormTextField,
agent_field: FormTextField,
available_agents: Vec<String>,
is_new: bool,
field: usize, // 0 name, 1 mode, 2 agents, 3 orch, 4 agent, 5 save, 6 cancel
field: usize, // 0 name, 1 mode, 2 agents, 3 orch, 4 save, 5 cancel
is_complete: bool,
app_event_tx: AppEventSender,
}
@@ -100,19 +99,19 @@ impl SubagentEditorView {
selected_agent_indices: Vec::new(),
agent_cursor: 0,
orch_field: FormTextField::new_multi_line(),
agent_field: FormTextField::new_multi_line(),
available_agents: root.available_agents.clone(),
is_new: name.is_empty(),
field: 0,
is_complete: false,
app_event_tx: root.app_event_tx.clone(),
};
// Always seed the name field with the provided name
if !name.is_empty() { me.name_field.set_text(name); }
// Seed from existing config if present
if let Some(cfg) = root.existing.iter().find(|c| c.name.eq_ignore_ascii_case(name)) {
me.name_field.set_text(&cfg.name);
me.read_only = cfg.read_only;
me.orch_field.set_text(&cfg.orchestrator_instructions.clone().unwrap_or_default());
me.agent_field.set_text(&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); }
@@ -123,19 +122,16 @@ impl SubagentEditorView {
"plan" => {
me.read_only = true;
me.orch_field.set_text("Plan a multi-agent approach. Research the repo structure, enumerate tasks, dependencies, risks, and milestones. Use multiple agents in read-only mode and synthesize a single, actionable plan.");
me.agent_field.set_text("Study the codebase, cite files you read, list assumptions. Propose concrete steps and call out risks or unknowns.");
}
"solve" => {
me.read_only = true;
me.orch_field.set_text("Coordinate multiple agents to propose competing solutions. Keep all agents read-only. Compare proposals, pick one, and outline verification steps.");
me.agent_field.set_text("Propose a fix with exact steps to validate. Be explicit about tests to run and edge cases to check.");
}
"code" => {
me.read_only = false;
me.orch_field.set_text("Coordinate write-mode agents to implement the task in isolated worktrees. Surface worktree_path and branch_name after completion.");
me.agent_field.set_text("Make minimal, focused changes with clear rationale. Include tests or validation steps where possible.");
}
_ => { me.name_field.set_text(name); }
_ => {}
}
}
me
@@ -176,10 +172,7 @@ impl SubagentEditorView {
let t = self.orch_field.text().trim().to_string();
if t.is_empty() { None } else { Some(t) }
},
agent_instructions: {
let t = self.agent_field.text().trim().to_string();
if t.is_empty() { None } else { Some(t) }
},
agent_instructions: None,
};
// Persist to disk asynchronously to avoid blocking the TUI runtime
if let Ok(home) = codex_core::config::find_codex_home() {
@@ -197,17 +190,17 @@ 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, .. } => { self.is_complete = true; }
KeyEvent { code: KeyCode::Tab, .. } => { self.field = (self.field + 1).min(6); }
KeyEvent { code: KeyCode::Tab, .. } => { self.field = (self.field + 1).min(5); }
KeyEvent { code: KeyCode::BackTab, .. } => { if self.field > 0 { self.field -= 1; } }
KeyEvent { code: KeyCode::Up, modifiers, .. } => {
if (self.field == 3 || self.field == 4) && modifiers.contains(KeyModifiers::SHIFT) {
let _ = match self.field { 3 => self.orch_field.handle_key(key_event), 4 => self.agent_field.handle_key(key_event), _ => false };
if self.field == 3 && modifiers.contains(KeyModifiers::SHIFT) {
let _ = self.orch_field.handle_key(key_event);
} else if self.field > 0 { self.field -= 1; }
}
KeyEvent { code: KeyCode::Down, modifiers, .. } => {
if (self.field == 3 || self.field == 4) && modifiers.contains(KeyModifiers::SHIFT) {
let _ = match self.field { 3 => self.orch_field.handle_key(key_event), 4 => self.agent_field.handle_key(key_event), _ => false };
} else { self.field = (self.field + 1).min(6); }
if self.field == 3 && modifiers.contains(KeyModifiers::SHIFT) {
let _ = self.orch_field.handle_key(key_event);
} else { self.field = (self.field + 1).min(5); }
}
KeyEvent { code: KeyCode::Left, .. } if self.field == 1 => { self.read_only = !self.read_only; }
KeyEvent { code: KeyCode::Right, .. } if self.field == 1 => { self.read_only = !self.read_only; }
@@ -225,9 +218,8 @@ impl<'a> BottomPaneView<'a> for SubagentEditorView {
// Delegate input to focused text fields (handles Shift‑chars, Enter/newline, undo, etc.)
ev @ KeyEvent { .. } if self.field == 0 => { let _ = self.name_field.handle_key(ev); }
ev @ KeyEvent { .. } if self.field == 3 => { let _ = self.orch_field.handle_key(ev); }
ev @ KeyEvent { .. } if self.field == 4 => { let _ = self.agent_field.handle_key(ev); }
KeyEvent { code: KeyCode::Enter, .. } if self.field == 5 => { self.save(); }
KeyEvent { code: KeyCode::Enter, .. } if self.field == 6 => { self.is_complete = true; }
KeyEvent { code: KeyCode::Enter, .. } if self.field == 4 => { self.save(); }
KeyEvent { code: KeyCode::Enter, .. } if self.field == 5 => { self.is_complete = true; }
_ => {}
}
}
@@ -235,12 +227,7 @@ impl<'a> BottomPaneView<'a> for SubagentEditorView {
fn is_complete(&self) -> bool { self.is_complete }
fn handle_paste(&mut self, text: String) -> super::bottom_pane_view::ConditionalUpdate {
match self.field {
0 => self.name_field.handle_paste(text),
3 => self.orch_field.handle_paste(text),
4 => self.agent_field.handle_paste(text),
_ => {}
}
match self.field { 0 => self.name_field.handle_paste(text), 3 => self.orch_field.handle_paste(text), _ => {} }
super::bottom_pane_view::ConditionalUpdate::NeedsRedraw
}
@@ -249,16 +236,12 @@ impl<'a> BottomPaneView<'a> for SubagentEditorView {
let inner_w = width.saturating_sub(2);
let content_w = inner_w.saturating_sub(1).max(10) as usize;
// Static rows (content lines):
// Name(1), Mode(1), Agents label(1), Agents line(1),
// Orchestrator label(1), Agent label(1), Buttons(1)
let static_rows: u16 = 7;
// Content rows for the two instruction bodies (wrapped)
// Name(1), Mode(1), Agents label(1), Agents line(1), Orchestrator label(1), Buttons(1)
let static_rows: u16 = 6;
// Content rows for the instruction body (wrapped)
let orch_rows = self.orch_field.desired_height(content_w as u16);
let agent_rows = self.agent_field.desired_height(content_w as u16);
// Sum and add borders (2)
let content_rows = static_rows
.saturating_add(orch_rows)
.saturating_add(agent_rows);
let content_rows = static_rows.saturating_add(orch_rows);
(content_rows + 2).clamp(8, 50)
}
@@ -294,16 +277,16 @@ impl<'a> BottomPaneView<'a> for SubagentEditorView {
lines.push(Line::from(Span::styled("Agents:", label(2))));
lines.push(Line::from(spans));
// Orchestrator label (content drawn below)
// Orchestrator label (content will be drawn in reserved blank rows below)
lines.push(Line::from(Span::styled("Instructions to Code (orchestrator):", label(3))));
lines.push(Line::from(""));
// Reserve as many blank lines as needed for the field height
let inner = Block::default().inner(area);
let content_rect = Rect { x: inner.x.saturating_add(1), y: inner.y, width: inner.width.saturating_sub(1), height: inner.height };
let orch_h = self.orch_field.desired_height(content_rect.width);
for _ in 0..orch_h.max(1) { lines.push(Line::from("")); }
// Agent label (content drawn below)
lines.push(Line::from(Span::styled("Instructions to each agent:", label(4))));
lines.push(Line::from(""));
let save_style = sel(5).fg(crate::colors::success());
let cancel_style = sel(6).fg(crate::colors::error());
let save_style = sel(4).fg(crate::colors::success());
let cancel_style = sel(5).fg(crate::colors::error());
lines.push(Line::from(vec![
Span::styled("[ Save ] ", save_style),
Span::styled("[ Cancel ]", cancel_style),
@@ -329,16 +312,10 @@ impl<'a> BottomPaneView<'a> for SubagentEditorView {
// Row 1: Mode; Row 2: Agents label; Row 3: Agents list
y = y.saturating_add(3);
// Row 4: Orchestrator label; draw field starting next row
// Row 4: Orchestrator label; draw field starting next row (reserved area)
y = y.saturating_add(1);
let orch_h = self.orch_field.desired_height(content_w);
self.orch_field.render(Rect { x: content_rect.x, y, width: content_w, height: orch_h }, buf, self.field == 3);
y = y.saturating_add(orch_h);
// Next row is Agent label; draw field starting the row after
y = y.saturating_add(1);
let agent_h = self.agent_field.desired_height(content_w);
self.agent_field.render(Rect { x: content_rect.x, y, width: content_w, height: agent_h }, buf, self.field == 4);
let orch_h2 = self.orch_field.desired_height(content_w);
self.orch_field.render(Rect { x: content_rect.x, y, width: content_w, height: orch_h2 }, buf, self.field == 3);
}
}