feat(tui): wire approval prompt with key handlers
- ApprovalPrompt gains call_id for routing decisions
- Up/Down navigates options, Enter selects
- "yes, always allow {tool}" sends ApprovedAlways
- Input/cursor blocked while approval prompt is active
- AgentEvent::ApprovalNeeded populates the prompt
This commit is contained in:
@@ -104,7 +104,7 @@ async fn cmd_code_inner(cmd: Option<CodeCommand>) -> anyhow::Result<()> {
|
||||
|
||||
// Spawn agent on background task
|
||||
let project_path = project.path.clone();
|
||||
let agent = agent::spawn(session, endpoint.clone());
|
||||
let agent = agent::spawn(session, endpoint.clone(), cfg, project.path.clone());
|
||||
|
||||
// TUI event loop — never blocks on network I/O
|
||||
use crossterm::event::{self, Event, KeyCode, KeyModifiers, MouseEventKind};
|
||||
@@ -133,12 +133,28 @@ async fn cmd_code_inner(cmd: Option<CodeCommand>) -> anyhow::Result<()> {
|
||||
// 1. Process any pending agent events (non-blocking)
|
||||
for evt in agent.poll_events() {
|
||||
match evt {
|
||||
agent::AgentEvent::Generating => {
|
||||
app.is_thinking = true;
|
||||
app.sol_status = "generating…".into();
|
||||
agent::AgentEvent::ApprovalNeeded { call_id, name, args_summary } => {
|
||||
app.approval = Some(tui::ApprovalPrompt {
|
||||
call_id: call_id.clone(),
|
||||
tool_name: name.clone(),
|
||||
command: args_summary.clone(),
|
||||
options: vec![
|
||||
"yes".into(),
|
||||
format!("yes, always allow {name}"),
|
||||
"no".into(),
|
||||
],
|
||||
selected: 0,
|
||||
});
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
agent::AgentEvent::ToolStart { name, detail } => {
|
||||
agent::AgentEvent::Generating => {
|
||||
app.is_thinking = true;
|
||||
app.sol_status.clear();
|
||||
app.thinking_message = tui::random_sol_status().to_string();
|
||||
app.thinking_since = Some(std::time::Instant::now());
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
agent::AgentEvent::ToolExecuting { name, detail } => {
|
||||
app.push_log(tui::LogEntry::ToolExecuting { name, detail });
|
||||
}
|
||||
agent::AgentEvent::ToolDone { name, success } => {
|
||||
@@ -150,14 +166,19 @@ async fn cmd_code_inner(cmd: Option<CodeCommand>) -> anyhow::Result<()> {
|
||||
app.sol_status = message;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
agent::AgentEvent::Response { text } => {
|
||||
agent::AgentEvent::Response { text, input_tokens, output_tokens } => {
|
||||
app.is_thinking = false;
|
||||
app.sol_status.clear();
|
||||
app.thinking_since = None;
|
||||
app.last_turn_tokens = input_tokens + output_tokens;
|
||||
app.input_tokens += input_tokens;
|
||||
app.output_tokens += output_tokens;
|
||||
app.push_log(tui::LogEntry::AssistantText(text));
|
||||
}
|
||||
agent::AgentEvent::Error { message } => {
|
||||
app.is_thinking = false;
|
||||
app.sol_status.clear();
|
||||
app.thinking_since = None;
|
||||
app.push_log(tui::LogEntry::Error(message));
|
||||
}
|
||||
agent::AgentEvent::Health { connected } => {
|
||||
@@ -172,14 +193,15 @@ async fn cmd_code_inner(cmd: Option<CodeCommand>) -> anyhow::Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Draw only when something changed
|
||||
if app.needs_redraw {
|
||||
// 2. Draw only when something changed (or animating)
|
||||
if app.needs_redraw || app.is_thinking {
|
||||
terminal.draw(|frame| tui::draw(frame, &mut app))?;
|
||||
app.needs_redraw = false;
|
||||
}
|
||||
|
||||
// 3. Handle input — drain ALL pending events before next draw
|
||||
if event::poll(std::time::Duration::from_millis(50))? {
|
||||
// 3. Handle input — shorter poll when animating
|
||||
let poll_ms = if app.is_thinking { 100 } else { 50 };
|
||||
if event::poll(std::time::Duration::from_millis(poll_ms))? {
|
||||
// Drain all queued events in one batch (coalesces rapid scroll)
|
||||
while event::poll(std::time::Duration::ZERO)? {
|
||||
match event::read()? {
|
||||
@@ -220,20 +242,48 @@ async fn cmd_code_inner(cmd: Option<CodeCommand>) -> anyhow::Result<()> {
|
||||
app.show_logs = !app.show_logs;
|
||||
app.log_scroll = u16::MAX;
|
||||
}
|
||||
KeyCode::Char(c) if !app.show_logs => {
|
||||
// Approval prompt navigation
|
||||
KeyCode::Up if app.approval.is_some() => {
|
||||
if let Some(ref mut a) = app.approval {
|
||||
a.selected = a.selected.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
KeyCode::Down if app.approval.is_some() => {
|
||||
if let Some(ref mut a) = app.approval {
|
||||
a.selected = (a.selected + 1).min(a.options.len() - 1);
|
||||
}
|
||||
}
|
||||
KeyCode::Enter if app.approval.is_some() => {
|
||||
if let Some(a) = app.approval.take() {
|
||||
let decision = match a.selected {
|
||||
0 => agent::ApprovalDecision::Approved {
|
||||
call_id: a.call_id.clone(),
|
||||
},
|
||||
1 => agent::ApprovalDecision::ApprovedAlways {
|
||||
call_id: a.call_id.clone(),
|
||||
tool_name: a.tool_name.clone(),
|
||||
},
|
||||
_ => agent::ApprovalDecision::Denied {
|
||||
call_id: a.call_id.clone(),
|
||||
},
|
||||
};
|
||||
agent.decide(decision);
|
||||
}
|
||||
}
|
||||
KeyCode::Char(c) if !app.show_logs && app.approval.is_none() => {
|
||||
app.history_index = None;
|
||||
app.input.insert(app.cursor_pos, c);
|
||||
app.cursor_pos += 1;
|
||||
}
|
||||
KeyCode::Backspace if !app.show_logs => {
|
||||
KeyCode::Backspace if !app.show_logs && app.approval.is_none() => {
|
||||
if app.cursor_pos > 0 {
|
||||
app.history_index = None;
|
||||
app.cursor_pos -= 1;
|
||||
app.input.remove(app.cursor_pos);
|
||||
}
|
||||
}
|
||||
KeyCode::Left if !app.show_logs => app.cursor_pos = app.cursor_pos.saturating_sub(1),
|
||||
KeyCode::Right if !app.show_logs => app.cursor_pos = (app.cursor_pos + 1).min(app.input.len()),
|
||||
KeyCode::Left if !app.show_logs && app.approval.is_none() => app.cursor_pos = app.cursor_pos.saturating_sub(1),
|
||||
KeyCode::Right if !app.show_logs && app.approval.is_none() => app.cursor_pos = (app.cursor_pos + 1).min(app.input.len()),
|
||||
KeyCode::Up if !app.show_logs => {
|
||||
if !app.command_history.is_empty() {
|
||||
let idx = match app.history_index {
|
||||
|
||||
Reference in New Issue
Block a user