Skip to content

Commit 15c2044

Browse files
fix(agent_ui): Disable send when tool awaits confirmation (#44211)
Workaround for API error 400 (duplicate tool_use IDs) when sending a message while a tool awaits confirmation. The root cause is not yet identified and requires further investigation. This fix prevents the problematic scenario at the UI level by blocking message sending when a tool awaits confirmation. Changes: - Add has_tool_awaiting_confirmation() helper method - Block send() when a tool awaits confirmation - Disable send button visually with informative tooltip Users must click Accept/Reject on tool confirmations before sending new messages. Fixes #44211
1 parent 9860884 commit 15c2044

File tree

1 file changed

+83
-2
lines changed

1 file changed

+83
-2
lines changed

crates/agent_ui/src/acp/thread_view.rs

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,12 @@ impl AcpThreadView {
10051005
matches!(self.thread_state, ThreadState::Loading { .. })
10061006
}
10071007

1008+
fn has_tool_awaiting_confirmation(&self, cx: &App) -> bool {
1009+
self.thread()
1010+
.map(|t| t.read(cx).first_tool_awaiting_confirmation().is_some())
1011+
.unwrap_or(false)
1012+
}
1013+
10081014
fn resume_chat(&mut self, cx: &mut Context<Self>) {
10091015
self.thread_error.take();
10101016
let Some(thread) = self.thread() else {
@@ -1034,6 +1040,13 @@ impl AcpThreadView {
10341040
return;
10351041
}
10361042

1043+
// Block sending when a tool is awaiting confirmation to prevent
1044+
// duplicate tool_use IDs error in Claude Code SDK (fixes #44211).
1045+
// User must respond to tool confirmation first.
1046+
if self.has_tool_awaiting_confirmation(cx) {
1047+
return;
1048+
}
1049+
10371050
self.history_store.update(cx, |history, cx| {
10381051
history.push_recently_opened_entry(
10391052
HistoryEntryId::AcpThread(thread.read(cx).session_id().clone()),
@@ -4468,6 +4481,7 @@ impl AcpThreadView {
44684481
let is_generating = self
44694482
.thread()
44704483
.is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle);
4484+
let has_tool_awaiting = self.has_tool_awaiting_confirmation(cx);
44714485

44724486
if self.is_loading_contents {
44734487
div()
@@ -4486,7 +4500,9 @@ impl AcpThreadView {
44864500
.on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx)))
44874501
.into_any_element()
44884502
} else {
4489-
let send_btn_tooltip = if is_editor_empty && !is_generating {
4503+
let send_btn_tooltip = if has_tool_awaiting {
4504+
"Respond to Tool Confirmation First"
4505+
} else if is_editor_empty && !is_generating {
44904506
"Type to Send"
44914507
} else if is_generating {
44924508
"Stop and Send Message"
@@ -4497,7 +4513,10 @@ impl AcpThreadView {
44974513
IconButton::new("send-message", IconName::Send)
44984514
.style(ButtonStyle::Filled)
44994515
.map(|this| {
4500-
if is_editor_empty && !is_generating {
4516+
// Disable send button when:
4517+
// - Editor is empty and not generating, OR
4518+
// - A tool is awaiting confirmation (prevents duplicate tool_use IDs, #44211)
4519+
if (is_editor_empty && !is_generating) || has_tool_awaiting {
45014520
this.disabled(true).icon_color(Color::Muted)
45024521
} else {
45034522
this.icon_color(Color::Accent)
@@ -6877,6 +6896,68 @@ pub(crate) mod tests {
68776896
));
68786897
}
68796898

6899+
#[gpui::test]
6900+
async fn test_message_doesnt_send_if_tool_awaiting_confirmation(cx: &mut TestAppContext) {
6901+
init_test(cx);
6902+
6903+
let tool_call_id = acp::ToolCallId::new("1");
6904+
let tool_call = acp::ToolCall::new(tool_call_id.clone(), "ExitPlanMode")
6905+
.kind(acp::ToolKind::Edit)
6906+
.content(vec!["Planning complete".into()]);
6907+
let connection =
6908+
StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
6909+
tool_call_id,
6910+
vec![acp::PermissionOption::new(
6911+
"1".into(),
6912+
"Allow",
6913+
acp::PermissionOptionKind::AllowOnce,
6914+
)],
6915+
)]));
6916+
6917+
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
6918+
6919+
let (thread_view, cx) =
6920+
setup_thread_view(StubAgentServer::new(connection.clone()), cx).await;
6921+
add_to_workspace(thread_view.clone(), cx);
6922+
6923+
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
6924+
message_editor.update_in(cx, |editor, window, cx| {
6925+
editor.set_text("Initial message", window, cx);
6926+
});
6927+
6928+
thread_view.update_in(cx, |thread_view, window, cx| {
6929+
thread_view.send(window, cx);
6930+
});
6931+
6932+
cx.run_until_parked();
6933+
6934+
let has_tool_awaiting =
6935+
thread_view.read_with(cx, |view, cx| view.has_tool_awaiting_confirmation(cx));
6936+
assert!(
6937+
has_tool_awaiting,
6938+
"Expected a tool to be awaiting confirmation after initial send"
6939+
);
6940+
6941+
let mut events = cx.events(&message_editor);
6942+
message_editor.update_in(cx, |editor, window, cx| {
6943+
editor.set_text("Second message while tool awaits", window, cx);
6944+
});
6945+
6946+
thread_view.update_in(cx, |thread_view, window, cx| {
6947+
thread_view.send(window, cx);
6948+
});
6949+
6950+
cx.run_until_parked();
6951+
6952+
assert!(
6953+
matches!(
6954+
events.try_next(),
6955+
Err(futures::channel::mpsc::TryRecvError { .. })
6956+
),
6957+
"Message should not be sent when a tool is awaiting confirmation (fixes #44211)"
6958+
);
6959+
}
6960+
68806961
#[gpui::test]
68816962
async fn test_message_editing_regenerate(cx: &mut TestAppContext) {
68826963
init_test(cx);

0 commit comments

Comments
 (0)