@@ -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