fix: carry tool input in ApprovalRequired event so approval dialog always shows params

The ApprovalRequest params_display() relied on pending_tool_uses,
which gets drained by MessageComplete before ApprovalRequired arrives
in the TUI event loop. When the model streaming response contained a
text block, ContentBlockStop(Text) set pending_message_complete=true,
triggering the drain and leaving the approval dialog with empty {}.

Fix: add input: Value to Event::ApprovalRequired and populate it
from tool_input in the engine tool execution loop. The TUI now reads
params directly from the event instead of searching pending_tool_uses.
This commit is contained in:
jayzhu
2026-05-28 23:09:03 +08:00
parent 54151a4bc9
commit bfb0ccc9b4
4 changed files with 10 additions and 6 deletions
+1
View File
@@ -1696,6 +1696,7 @@ impl Engine {
.send(Event::ApprovalRequired {
id: tool_id.clone(),
tool_name: tool_name.clone(),
input: tool_input.clone(),
description: plan.approval_description.clone(),
approval_key,
approval_grouping_key,
+3
View File
@@ -226,6 +226,9 @@ pub enum Event {
id: String,
tool_name: String,
description: String,
/// Tool parameters for approval display. Carried on the event so the
/// TUI does not need to reconstruct them from `pending_tool_uses`.
input: Value,
/// Exact-argument fingerprint, used to scope *denials* (#1617).
approval_key: String,
/// Lossy / arity-aware fingerprint, used to scope *approvals* so an
+4
View File
@@ -4172,6 +4172,7 @@ mod tests {
id: "tool_stale".to_string(),
tool_name: "exec_command".to_string(),
description: "stale approval".to_string(),
input: serde_json::json!({}),
})
.await?;
@@ -4245,6 +4246,7 @@ mod tests {
id: "tool_external_allow".to_string(),
tool_name: "exec_command".to_string(),
description: "external allow".to_string(),
input: serde_json::json!({}),
})
.await?;
@@ -4322,6 +4324,7 @@ mod tests {
id: "tool_external_deny".to_string(),
tool_name: "exec_command".to_string(),
description: "external deny".to_string(),
input: serde_json::json!({}),
})
.await?;
@@ -4508,6 +4511,7 @@ mod tests {
id: "tool_remember".to_string(),
tool_name: "exec_command".to_string(),
description: "remember=true".to_string(),
input: serde_json::json!({}),
})
.await?;
+2 -6
View File
@@ -1893,6 +1893,7 @@ async fn run_event_loop(
id,
tool_name,
description,
input,
approval_key,
approval_grouping_key,
} => {
@@ -1937,12 +1938,7 @@ async fn run_event_loop(
app.status_message =
Some(format!("Blocked tool '{tool_name}' (approval_mode=never)"));
} else {
let tool_input = app
.pending_tool_uses
.iter()
.find(|(tool_id, _, _)| tool_id == &id)
.map(|(_, _, input)| input.clone())
.unwrap_or_else(|| serde_json::json!({}));
let tool_input = input;
if tool_name == "apply_patch" {
maybe_add_patch_preview(app, &tool_input);