77 QVBoxLayout ,
88 QPushButton ,
99 QLabel ,
10+ QScrollArea ,
1011)
1112from PySide6 .QtGui import QKeySequence , QShortcut
1213from PySide6 .QtCore import Qt
1314from AgentCrew .modules .gui .widgets .tool_widget import ToolWidget
15+ from AgentCrew .modules .gui .widgets .diff_widget import DiffWidget
1416
1517
1618class ToolEventHandler :
@@ -158,17 +160,18 @@ def handle_tool_confirmation_required(self, tool_info):
158160 tool_use = tool_info .copy ()
159161 confirmation_id = tool_use .pop ("confirmation_id" )
160162
161- # Special handling for 'ask' tool
162163 if tool_use ["name" ] == "ask" :
163164 self ._handle_ask_tool_confirmation (tool_use , confirmation_id )
164165 return
165166
166- # Create dialog
167+ if tool_use ["name" ] == "write_or_edit_file" :
168+ self ._handle_write_or_edit_file_confirmation (tool_use , confirmation_id )
169+ return
170+
167171 dialog = QMessageBox (self .chat_window )
168172 dialog .setWindowTitle ("Tool Execution Confirmation" )
169173 dialog .setIcon (QMessageBox .Icon .Question )
170174
171- # Format tool information for display
172175 tool_description = f"The assistant wants to use the '{ tool_use ['name' ]} ' tool."
173176 params_text = ""
174177
@@ -189,7 +192,6 @@ def handle_tool_confirmation_required(self, tool_info):
189192 text_edit .setReadOnly (True )
190193 text_edit .setText (params_text )
191194
192- # Style the text edit to match the main theme
193195 text_edit .setStyleSheet (
194196 self .chat_window .style_provider .get_tool_dialog_text_edit_style ()
195197 )
@@ -293,6 +295,167 @@ def handle_tool_denied(self, data):
293295 self .chat_window .current_response_bubble = None
294296 self .chat_window .current_response_container = None
295297
298+ def _handle_write_or_edit_file_confirmation (self , tool_use , confirmation_id ):
299+ """Handle write_or_edit_file tool confirmation with diff view."""
300+ from PySide6 .QtWidgets import QWidget , QHBoxLayout
301+
302+ tool_input = tool_use .get ("input" , {})
303+ file_path = tool_input .get ("file_path" , "" )
304+ text_or_blocks = tool_input .get ("text_or_search_replace_blocks" , "" )
305+ percentage = tool_input .get ("percentage_to_change" , 0 )
306+
307+ has_diff = DiffWidget .has_search_replace_blocks (text_or_blocks )
308+
309+ dialog = QDialog (self .chat_window )
310+ dialog .setWindowTitle ("File Edit Confirmation" )
311+ dialog .setMinimumWidth (800 if has_diff else 600 )
312+ dialog .setMinimumHeight (600 if has_diff else 400 )
313+
314+ layout = QVBoxLayout ()
315+
316+ diff_colors = self .chat_window .style_provider .get_diff_colors ()
317+ header_label = QLabel (f"📝 <b>Edit File:</b> { file_path } " )
318+ header_label .setStyleSheet (
319+ f"font-size: 14px; padding: 10px; color: { diff_colors .get ('header_text' , '#89b4fa' )} ;"
320+ )
321+ layout .addWidget (header_label )
322+
323+ info_label = QLabel (
324+ f"Change percentage: { percentage } % | "
325+ f"Mode: { 'Search/Replace Blocks' if has_diff else 'Full Content' } "
326+ )
327+ info_label .setStyleSheet (
328+ f"font-size: 11px; color: { diff_colors .get ('line_number_text' , '#6c7086' )} ; padding: 0 10px;"
329+ )
330+ layout .addWidget (info_label )
331+
332+ scroll_area = QScrollArea ()
333+ scroll_area .setWidgetResizable (True )
334+ scroll_area .setMinimumHeight (350 )
335+
336+ if has_diff :
337+ diff_widget = DiffWidget (style_provider = self .chat_window .style_provider )
338+ diff_widget .set_diff_content (text_or_blocks , file_path )
339+ scroll_area .setWidget (diff_widget )
340+ else :
341+ content_widget = QTextEdit ()
342+ content_widget .setReadOnly (True )
343+ content_widget .setText (text_or_blocks )
344+ content_widget .setStyleSheet (
345+ self .chat_window .style_provider .get_tool_dialog_text_edit_style ()
346+ )
347+ scroll_area .setWidget (content_widget )
348+
349+ layout .addWidget (scroll_area )
350+
351+ buttons_container = QWidget ()
352+ buttons_layout = QHBoxLayout (buttons_container )
353+ buttons_layout .setContentsMargins (0 , 10 , 0 , 0 )
354+
355+ yes_button = QPushButton ("✓ Approve" )
356+ yes_button .setStyleSheet (
357+ self .chat_window .style_provider .get_tool_dialog_yes_button_style ()
358+ )
359+
360+ all_button = QPushButton ("✓✓ Yes to All" )
361+ all_button .setStyleSheet (
362+ self .chat_window .style_provider .get_tool_dialog_all_button_style ()
363+ )
364+
365+ forever_button = QPushButton ("∞ Forever" )
366+ forever_button .setStyleSheet (
367+ self .chat_window .style_provider .get_tool_dialog_all_button_style ()
368+ )
369+
370+ no_button = QPushButton ("✗ Deny" )
371+ no_button .setStyleSheet (
372+ self .chat_window .style_provider .get_tool_dialog_no_button_style ()
373+ )
374+
375+ buttons_layout .addWidget (yes_button )
376+ buttons_layout .addWidget (all_button )
377+ buttons_layout .addWidget (forever_button )
378+ buttons_layout .addStretch ()
379+ buttons_layout .addWidget (no_button )
380+
381+ layout .addWidget (buttons_container )
382+
383+ dialog .setLayout (layout )
384+ dialog .setStyleSheet (self .chat_window .style_provider .get_config_window_style ())
385+
386+ result = {"action" : "" }
387+
388+ def on_yes ():
389+ result ["action" ] = "approve"
390+ dialog .accept ()
391+
392+ def on_all ():
393+ result ["action" ] = "approve_all"
394+ dialog .accept ()
395+
396+ def on_forever ():
397+ result ["action" ] = "approve_forever"
398+ dialog .accept ()
399+
400+ def on_no ():
401+ result ["action" ] = "deny"
402+ dialog .reject ()
403+
404+ yes_button .clicked .connect (on_yes )
405+ all_button .clicked .connect (on_all )
406+ forever_button .clicked .connect (on_forever )
407+ no_button .clicked .connect (on_no )
408+
409+ approve_shortcut = QShortcut (QKeySequence ("Ctrl+Return" ), dialog )
410+ approve_shortcut .activated .connect (on_yes )
411+
412+ dialog .exec ()
413+
414+ if result ["action" ] == "approve" :
415+ self .chat_window .message_handler .resolve_tool_confirmation (
416+ confirmation_id , {"action" : "approve" }
417+ )
418+ self .chat_window .display_status_message (f"Approved file edit: { file_path } " )
419+
420+ elif result ["action" ] == "approve_all" :
421+ self .chat_window .message_handler .resolve_tool_confirmation (
422+ confirmation_id , {"action" : "approve_all" }
423+ )
424+ self .chat_window .display_status_message (
425+ "Approved all future write_or_edit_file calls"
426+ )
427+
428+ elif result ["action" ] == "approve_forever" :
429+ from AgentCrew .modules .config import ConfigManagement
430+
431+ config_manager = ConfigManagement ()
432+ config_manager .write_auto_approval_tools ("write_or_edit_file" , add = True )
433+
434+ self .chat_window .message_handler .resolve_tool_confirmation (
435+ confirmation_id , {"action" : "approve_all" }
436+ )
437+ self .chat_window .display_status_message (
438+ "write_or_edit_file will be auto-approved forever"
439+ )
440+
441+ else :
442+ denial_reason = self .show_denial_reason_dialog ("write_or_edit_file" )
443+
444+ if denial_reason :
445+ self .chat_window .message_handler .resolve_tool_confirmation (
446+ confirmation_id , {"action" : "deny" , "reason" : denial_reason }
447+ )
448+ self .chat_window .display_status_message (
449+ f"Denied file edit - Reason: { denial_reason [:50 ]} ..."
450+ if len (denial_reason ) > 50
451+ else f"Denied file edit - Reason: { denial_reason } "
452+ )
453+ else :
454+ self .chat_window .message_handler .resolve_tool_confirmation (
455+ confirmation_id , {"action" : "deny" }
456+ )
457+ self .chat_window .display_status_message ("Denied file edit" )
458+
296459 def _handle_ask_tool_confirmation (self , tool_use , confirmation_id ):
297460 """Handle the ask tool - display question and guided answers in GUI."""
298461 question = tool_use ["input" ].get ("question" , "" )
0 commit comments