Commit ae939c6

mo khan <mo@mokhan.ca>
2025-06-22 15:29:08
fix: parse tool calls for the different formats
1 parent e3290b9
Changed files (2)
cmd/del/main.go
@@ -23,8 +23,7 @@ Help developers write better, safer, and more efficient code through intelligent
 
 # CORE BEHAVIOR
 
-1. **Smart Tool Usage**: When users need file operations, code analysis, or system commands, use tools automatically:
-   TOOL_USE: tool_name {JSON args}
+1. **Smart Tool Usage**: When users need file operations, code analysis, or system commands, use your available function calling capabilities to invoke the appropriate tools automatically. **IMPORTANT: STOP immediately after making tool calls - do NOT generate fake results or continue the response.**
 
 2. **Expert Coding Advice**: Provide insightful, actionable coding guidance when tools aren't needed
 
@@ -59,45 +58,38 @@ Help developers write better, safer, and more efficient code through intelligent
 - web_search: Search for information {"query": string}
 - test_runner: Run project tests {"type": string, "path": string}
 
+📝 Multi-File Operations:
+- multi_edit: Edit multiple files with find-replace {"operations": [{"file": string, "old": string, "new": string}]}
+- batch_operations: Execute multiple file operations {"operations": [{"type": "read|write|list", "path": string, "content": string}]}
+
 # INTERACTION STYLE
 
 ✅ DO:
 - Use tools when users need file ops, analysis, or commands
-- Give concise, expert coding advice
+- STOP immediately after making tool calls (let Del execute them)
+- Give concise, expert coding advice when tools aren't needed
 - Suggest best practices and optimizations
 - Be proactive about security and code quality
 
 ❌ DON'T:
+- Generate fake tool outputs or results
+- Continue responding after making tool calls
+- Add <|tool▁outputs▁begin|> or similar fake output blocks
 - Over-explain tool usage
-- Apologize unnecessarily  
-- Add markdown formatting to tool responses
-- Repeat the user's request
+- Apologize unnecessarily
 
 # EXAMPLES
 
-User: "show me the main function"
-TOOL_USE: read_file {"path": "main.go"}
-
-User: "what's the git status?"  
-TOOL_USE: run_command {"command": "git status"}
-
-User: "how do I optimize this algorithm?"
-[Provide expert algorithmic advice without tools]
-
-User: "scan this project for security issues"
-TOOL_USE: security_scan {"path": "."}
+When users make requests that require tools, automatically invoke the appropriate functions:
 
-User: "check git status"
-TOOL_USE: git_operation {"operation": "status"}
+User: "show me the main function" → read_file with path "main.go"
+User: "what's the git status?" → git_operation with operation "status"  
+User: "scan this project for security issues" → security_scan with path "."
+User: "run the tests" → test_runner to auto-detect and run tests
+User: "search for golang best practices" → web_search with relevant query
+User: "how do I optimize this algorithm?" → provide expert advice (no tools needed)
 
-User: "commit these changes with message fix bug"
-TOOL_USE: git_operation {"operation": "commit", "message": "fix bug"}
-
-User: "search for golang best practices"
-TOOL_USE: web_search {"query": "golang best practices"}
-
-User: "run the tests"
-TOOL_USE: test_runner {"type": "", "path": "."}
+Use your natural function calling format - Del supports all major tool calling standards.
 
 Stay funky, keep coding! 🎵`
 
@@ -107,10 +99,17 @@ type Tool struct {
 	Handler     func(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error)
 }
 
+type ToolCall struct {
+	Name string
+	Args map[string]interface{}
+}
+
 type Del struct {
-	aiProvider  AIProvider
-	tools       map[string]*Tool
-	chatHistory []api.Message
+	aiProvider       AIProvider
+	tools            map[string]*Tool
+	chatHistory      []api.Message
+	safeMode         bool
+	confirmedActions map[string]bool
 }
 
 type AIProvider interface {
@@ -791,6 +790,445 @@ func (d *Del) testRunner(ctx context.Context, args map[string]interface{}, ch ch
 	return result, nil
 }
 
+// multi_edit: Edit multiple files with find-replace operations
+func (d *Del) multiEdit(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+	operations, ok := args["operations"].([]interface{})
+	if !ok {
+		return nil, fmt.Errorf("missing 'operations' array argument")
+	}
+	
+	var results []string
+	var output strings.Builder
+	
+	output.WriteString("🔧 Multi-file editing:\n\n")
+	
+	for i, op := range operations {
+		opMap, ok := op.(map[string]interface{})
+		if !ok {
+			return nil, fmt.Errorf("operation %d is not a valid object", i)
+		}
+		
+		filePath, _ := opMap["file"].(string)
+		oldText, _ := opMap["old"].(string)
+		newText, _ := opMap["new"].(string)
+		
+		if filePath == "" || oldText == "" {
+			return nil, fmt.Errorf("operation %d missing required 'file' or 'old' fields", i)
+		}
+		
+		// Read the file
+		data, err := os.ReadFile(filePath)
+		if err != nil {
+			msg := fmt.Sprintf("❌ %s: failed to read (%v)\n", filePath, err)
+			output.WriteString(msg)
+			results = append(results, msg)
+			continue
+		}
+		
+		content := string(data)
+		
+		// Check if old text exists
+		if !strings.Contains(content, oldText) {
+			msg := fmt.Sprintf("⚠️  %s: text not found\n", filePath)
+			output.WriteString(msg)
+			results = append(results, msg)
+			continue
+		}
+		
+		// Perform replacement
+		newContent := strings.ReplaceAll(content, oldText, newText)
+		
+		// Write back to file
+		if err := os.WriteFile(filePath, []byte(newContent), 0644); err != nil {
+			msg := fmt.Sprintf("❌ %s: failed to write (%v)\n", filePath, err)
+			output.WriteString(msg)
+			results = append(results, msg)
+			continue
+		}
+		
+		msg := fmt.Sprintf("✅ %s: updated successfully\n", filePath)
+		output.WriteString(msg)
+		results = append(results, msg)
+	}
+	
+	result := output.String()
+	ch <- result
+	return result, nil
+}
+
+// batch_operations: Execute multiple file operations (read, write, list) in sequence
+func (d *Del) batchOperations(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
+	operations, ok := args["operations"].([]interface{})
+	if !ok {
+		return nil, fmt.Errorf("missing 'operations' array argument")
+	}
+	
+	var output strings.Builder
+	output.WriteString("📋 Batch operations:\n\n")
+	
+	for i, op := range operations {
+		opMap, ok := op.(map[string]interface{})
+		if !ok {
+			return nil, fmt.Errorf("operation %d is not a valid object", i)
+		}
+		
+		opType, _ := opMap["type"].(string)
+		path, _ := opMap["path"].(string)
+		
+		switch opType {
+		case "read":
+			if path == "" {
+				return nil, fmt.Errorf("operation %d: read requires 'path'", i)
+			}
+			data, err := os.ReadFile(path)
+			if err != nil {
+				output.WriteString(fmt.Sprintf("❌ Read %s: %v\n", path, err))
+			} else {
+				output.WriteString(fmt.Sprintf("📖 Read %s (%d bytes):\n%s\n\n", path, len(data), string(data)))
+			}
+			
+		case "list":
+			if path == "" {
+				path = "."
+			}
+			entries, err := os.ReadDir(path)
+			if err != nil {
+				output.WriteString(fmt.Sprintf("❌ List %s: %v\n", path, err))
+			} else {
+				output.WriteString(fmt.Sprintf("📁 List %s:\n", path))
+				for _, entry := range entries {
+					prefix := "📄"
+					if entry.IsDir() {
+						prefix = "📂"
+					}
+					output.WriteString(fmt.Sprintf("  %s %s\n", prefix, entry.Name()))
+				}
+				output.WriteString("\n")
+			}
+			
+		case "write":
+			content, _ := opMap["content"].(string)
+			if path == "" || content == "" {
+				return nil, fmt.Errorf("operation %d: write requires 'path' and 'content'", i)
+			}
+			if err := os.WriteFile(path, []byte(content), 0644); err != nil {
+				output.WriteString(fmt.Sprintf("❌ Write %s: %v\n", path, err))
+			} else {
+				output.WriteString(fmt.Sprintf("✅ Write %s (%d bytes)\n", path, len(content)))
+			}
+			
+		default:
+			return nil, fmt.Errorf("operation %d: unsupported type '%s'", i, opType)
+		}
+	}
+	
+	result := output.String()
+	ch <- result
+	return result, nil
+}
+
+// requiresConfirmation checks if an operation requires user confirmation
+func (d *Del) requiresConfirmation(operation string, args map[string]interface{}) bool {
+	if !d.safeMode {
+		return false
+	}
+	
+	// Define sensitive operations
+	sensitiveOps := map[string]bool{
+		"write_file":       true,
+		"multi_edit":       true,
+		"git_push":         true,
+		"git_commit":       true,
+		"run_command":      true,
+		"format_code":      true,
+	}
+	
+	// Check for sensitive git operations
+	if operation == "git_operation" {
+		if gitOp, ok := args["operation"].(string); ok {
+			return gitOp == "push" || gitOp == "commit" || gitOp == "reset" || gitOp == "clean"
+		}
+	}
+	
+	// Check for dangerous shell commands
+	if operation == "run_command" {
+		if cmd, ok := args["command"].(string); ok {
+			dangerousCommands := []string{"rm", "sudo", "mv", "cp", "chmod", "chown", "dd", "mkfs"}
+			cmdLower := strings.ToLower(cmd)
+			for _, dangerous := range dangerousCommands {
+				if strings.Contains(cmdLower, dangerous) {
+					return true
+				}
+			}
+		}
+	}
+	
+	return sensitiveOps[operation]
+}
+
+// askConfirmation prompts user for confirmation
+func (d *Del) askConfirmation(operation string, description string) bool {
+	actionKey := fmt.Sprintf("%s:%s", operation, description)
+	
+	// Check if already confirmed for this session
+	if confirmed, exists := d.confirmedActions[actionKey]; exists && confirmed {
+		return true
+	}
+	
+	fmt.Printf("\n⚠️  CONFIRMATION REQUIRED ⚠️\n")
+	fmt.Printf("Operation: %s\n", operation)
+	fmt.Printf("Description: %s\n", description)
+	fmt.Printf("Continue? (y/N): ")
+	
+	var response string
+	fmt.Scanln(&response)
+	
+	confirmed := strings.ToLower(strings.TrimSpace(response)) == "y"
+	d.confirmedActions[actionKey] = confirmed
+	
+	return confirmed
+}
+
+// parseChineseFormat handles Chinese model format: <|tool▁calls▁begin|>...
+func (d *Del) parseChineseFormat(response string) []ToolCall {
+	var calls []ToolCall
+	
+	// Simple pattern to match Chinese model tool calls
+	// This handles: function<|tool▁sep|>name {...}
+	re := regexp.MustCompile(`function<|tool▁sep|>(\w+)`)
+	matches := re.FindAllStringSubmatch(response, -1)
+	
+	for _, match := range matches {
+		if len(match) >= 2 {
+			toolName := match[1]
+			
+			// Look for JSON args after the function name
+			// Find the position after this match
+			startPos := strings.Index(response, match[0]) + len(match[0])
+			remaining := response[startPos:]
+			
+			// Look for JSON object - simple approach
+			var args map[string]interface{}
+			jsonStart := strings.Index(remaining, "{")
+			if jsonStart != -1 {
+				jsonEnd := strings.Index(remaining[jsonStart:], "}")
+				if jsonEnd != -1 {
+					jsonStr := remaining[jsonStart : jsonStart+jsonEnd+1]
+					json.Unmarshal([]byte(jsonStr), &args)
+				}
+			}
+			
+			if args == nil {
+				args = make(map[string]interface{})
+			}
+			
+			calls = append(calls, ToolCall{Name: toolName, Args: args})
+		}
+	}
+	
+	// Fallback: try simpler regex pattern  
+	if len(calls) == 0 {
+		re = regexp.MustCompile(`(?s)<|tool▁call▁begin|>.*?function<|tool▁sep|>(\w+)\s*(\{[^}]*\}?)`)
+		matches = re.FindAllStringSubmatch(response, -1)
+		
+		for _, match := range matches {
+			if len(match) >= 3 {
+				toolName := match[1]
+				argsJSON := strings.TrimSpace(match[2])
+				
+				var args map[string]interface{}
+				if argsJSON == "" || argsJSON == "{}" {
+					args = make(map[string]interface{})
+				} else {
+					json.Unmarshal([]byte(argsJSON), &args)
+					if args == nil {
+						args = make(map[string]interface{})
+					}
+				}
+				
+				calls = append(calls, ToolCall{Name: toolName, Args: args})
+			}
+		}
+	}
+	
+	return calls
+}
+
+// parseOpenAIXMLFormat handles OpenAI XML format: <tool_calls><invoke>...
+func (d *Del) parseOpenAIXMLFormat(response string) []ToolCall {
+	var calls []ToolCall
+	
+	// Pattern to match OpenAI XML tool calls
+	re := regexp.MustCompile(`(?s)<tool_calls>.*?<invoke name="(\w+)">(.*?)</invoke>.*?</tool_calls>`)
+	matches := re.FindAllStringSubmatch(response, -1)
+	
+	for _, match := range matches {
+		if len(match) >= 3 {
+			toolName := match[1]
+			paramsXML := match[2]
+			
+			// Parse parameters from XML
+			args := make(map[string]interface{})
+			paramRe := regexp.MustCompile(`<parameter name="([^"]+)">([^<]*)</parameter>`)
+			paramMatches := paramRe.FindAllStringSubmatch(paramsXML, -1)
+			
+			for _, paramMatch := range paramMatches {
+				if len(paramMatch) >= 3 {
+					args[paramMatch[1]] = paramMatch[2]
+				}
+			}
+			
+			calls = append(calls, ToolCall{Name: toolName, Args: args})
+		}
+	}
+	
+	return calls
+}
+
+// parseOpenAIJSONFormat handles OpenAI JSON format: [{"name": "...", "parameters": {...}}]
+func (d *Del) parseOpenAIJSONFormat(response string) []ToolCall {
+	var calls []ToolCall
+	
+	// Look for JSON array of tool calls
+	re := regexp.MustCompile(`(?s)\[(\{[^}]*"name"[^}]*\}(?:,\s*\{[^}]*"name"[^}]*\})*)\]`)
+	matches := re.FindAllStringSubmatch(response, -1)
+	
+	for _, match := range matches {
+		if len(match) >= 2 {
+			jsonStr := "[" + match[1] + "]"
+			
+			var toolCalls []map[string]interface{}
+			if err := json.Unmarshal([]byte(jsonStr), &toolCalls); err != nil {
+				continue
+			}
+			
+			for _, tc := range toolCalls {
+				if name, ok := tc["name"].(string); ok {
+					args := make(map[string]interface{})
+					if params, ok := tc["parameters"].(map[string]interface{}); ok {
+						args = params
+					}
+					calls = append(calls, ToolCall{Name: name, Args: args})
+				}
+			}
+		}
+	}
+	
+	return calls
+}
+
+// parseLlamaFormat handles Llama format: <function=name>args</function>
+func (d *Del) parseLlamaFormat(response string) []ToolCall {
+	var calls []ToolCall
+	
+	// Pattern to match Llama function calls
+	re := regexp.MustCompile(`(?s)<function=(\w+)>(\{[^}]*\}?)</function>`)
+	matches := re.FindAllStringSubmatch(response, -1)
+	
+	for _, match := range matches {
+		if len(match) >= 3 {
+			toolName := match[1]
+			argsJSON := match[2]
+			
+			var args map[string]interface{}
+			if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
+				continue // Skip invalid JSON
+			}
+			
+			calls = append(calls, ToolCall{Name: toolName, Args: args})
+		}
+	}
+	
+	return calls
+}
+
+// parseCustomFormat handles Del's legacy format: TOOL_USE: name args
+func (d *Del) parseCustomFormat(response string) []ToolCall {
+	var calls []ToolCall
+	
+	// Pattern to match custom TOOL_USE format
+	re := regexp.MustCompile(`(?s)TOOL_USE:\s*(\w+)\s*(\{.*?\})`)
+	matches := re.FindAllStringSubmatch(response, -1)
+	
+	for _, match := range matches {
+		if len(match) >= 3 {
+			toolName := match[1]
+			argsJSON := match[2]
+			
+			var args map[string]interface{}
+			if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
+				continue // Skip invalid JSON
+			}
+			
+			calls = append(calls, ToolCall{Name: toolName, Args: args})
+		}
+	}
+	
+	return calls
+}
+
+// parseMarkdownFormat handles markdown function calls: ## Function Call\nname(args)
+func (d *Del) parseMarkdownFormat(response string) []ToolCall {
+	var calls []ToolCall
+	
+	// Pattern to match markdown function calls
+	re := regexp.MustCompile(`(?s)##\s*(?:Function Call|Tool Call)[\s\S]*?(\w+)\s*\((\{[^}]*\}?)\)`)
+	matches := re.FindAllStringSubmatch(response, -1)
+	
+	for _, match := range matches {
+		if len(match) >= 3 {
+			toolName := match[1]
+			argsJSON := match[2]
+			
+			var args map[string]interface{}
+			if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
+				continue // Skip invalid JSON
+			}
+			
+			calls = append(calls, ToolCall{Name: toolName, Args: args})
+		}
+	}
+	
+	return calls
+}
+
+// parseToolCalls attempts to parse tool calls from response using all known formats
+func (d *Del) parseToolCalls(response string) []ToolCall {
+	// Try all known formats in order of likelihood
+	
+	// 1. Chinese models (DeepSeek, Qwen, etc.) - most common currently
+	if calls := d.parseChineseFormat(response); len(calls) > 0 {
+		return calls
+	}
+	
+	// 2. OpenAI XML format
+	if calls := d.parseOpenAIXMLFormat(response); len(calls) > 0 {
+		return calls
+	}
+	
+	// 3. OpenAI JSON format
+	if calls := d.parseOpenAIJSONFormat(response); len(calls) > 0 {
+		return calls
+	}
+	
+	// 4. Llama format
+	if calls := d.parseLlamaFormat(response); len(calls) > 0 {
+		return calls
+	}
+	
+	// 5. Markdown format
+	if calls := d.parseMarkdownFormat(response); len(calls) > 0 {
+		return calls
+	}
+	
+	// 6. Legacy custom format (for backward compatibility)
+	if calls := d.parseCustomFormat(response); len(calls) > 0 {
+		return calls
+	}
+	
+	return []ToolCall{}
+}
+
 // mcp_call (stub for now)
 func (d *Del) stubMCP(ctx context.Context, args map[string]interface{}, ch chan string) (interface{}, error) {
 	endpoint, _ := args["endpoint"].(string)
@@ -824,8 +1262,10 @@ When users say:
 Always assume "." (current directory) when no path is specified for file operations.`, cwd)
 
 	d := &Del{
-		aiProvider: provider,
-		tools:      make(map[string]*Tool),
+		aiProvider:       provider,
+		tools:            make(map[string]*Tool),
+		safeMode:         true, // Enable safe mode by default
+		confirmedActions: make(map[string]bool),
 		chatHistory: []api.Message{
 			{
 				Role:    "system", 
@@ -908,6 +1348,16 @@ Always assume "." (current directory) when no path is specified for file operati
 		Description: "Detect and run tests for various languages (Go, Node.js, Python, Rust)",
 		Handler:     d.testRunner,
 	}
+	d.tools["multi_edit"] = &Tool{
+		Name:        "multi_edit",
+		Description: "Edit multiple files with find-replace operations",
+		Handler:     d.multiEdit,
+	}
+	d.tools["batch_operations"] = &Tool{
+		Name:        "batch_operations",
+		Description: "Execute multiple file operations (read, write, list) in sequence",
+		Handler:     d.batchOperations,
+	}
 	d.tools["mcp_call"] = &Tool{
 		Name:        "mcp_call",
 		Description: "Stub for MCP integration",
@@ -920,50 +1370,70 @@ func (d *Del) streamChat(ctx context.Context, history []api.Message) (string, er
 	return d.aiProvider.StreamChat(ctx, history)
 }
 
-func (d *Del) handleToolCalls(ctx context.Context, response string) string {
-	re := regexp.MustCompile(`(?s)TOOL_USE:\s*(\w+)\s*(\{.*?\})`)
-	matches := re.FindAllStringSubmatch(response, -1)
-	if len(matches) == 0 {
-		return response
-	}
-	var finalOutput strings.Builder
-	for _, match := range matches {
-		toolName := match[1]
-		argsJSON := match[2]
-		tool, ok := d.tools[toolName]
+func (d *Del) executeToolCalls(ctx context.Context, toolCalls []ToolCall) map[string]interface{} {
+	results := make(map[string]interface{})
+	
+	fmt.Printf("\n🔧 Executing %d tool call(s)...\n", len(toolCalls))
+	
+	for i, toolCall := range toolCalls {
+		tool, ok := d.tools[toolCall.Name]
 		if !ok {
-			finalOutput.WriteString(fmt.Sprintf("[error] unknown tool: %s\n", toolName))
+			results[fmt.Sprintf("tool_%d", i)] = map[string]interface{}{
+				"tool": toolCall.Name,
+				"error": fmt.Sprintf("unknown tool: %s", toolCall.Name),
+			}
 			continue
 		}
-		var args map[string]interface{}
-		if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
-			finalOutput.WriteString(fmt.Sprintf("[error] bad args: %v\n", err))
-			continue
+		
+		// Check if confirmation is required
+		if d.requiresConfirmation(toolCall.Name, toolCall.Args) {
+			argsJSON, _ := json.Marshal(toolCall.Args)
+			description := fmt.Sprintf("%s with args: %s", toolCall.Name, string(argsJSON))
+			if !d.askConfirmation(toolCall.Name, description) {
+				results[fmt.Sprintf("tool_%d", i)] = map[string]interface{}{
+					"tool": toolCall.Name,
+					"error": "operation cancelled by user",
+				}
+				continue
+			}
 		}
+		
 		ch := make(chan string, 10)
 		go func() {
 			for line := range ch {
 				fmt.Print(line)
 			}
 		}()
-		result, err := tool.Handler(ctx, args, ch)
+		
+		result, err := tool.Handler(ctx, toolCall.Args, ch)
 		close(ch)
+		
 		if err != nil {
-			finalOutput.WriteString(fmt.Sprintf("[tool error] %v\n", err))
-			continue
-		}
-		switch val := result.(type) {
-		case string:
-			finalOutput.WriteString(val)
-			if !strings.HasSuffix(val, "\n") {
-				finalOutput.WriteString("\n")
+			results[fmt.Sprintf("tool_%d", i)] = map[string]interface{}{
+				"tool": toolCall.Name,
+				"error": err.Error(),
+			}
+		} else {
+			results[fmt.Sprintf("tool_%d", i)] = map[string]interface{}{
+				"tool": toolCall.Name,
+				"result": result,
 			}
-		default:
-			outBytes, _ := json.MarshalIndent(val, "", "  ")
-			finalOutput.WriteString(string(outBytes) + "\n")
 		}
 	}
-	return finalOutput.String()
+	
+	return results
+}
+
+func (d *Del) handleToolCalls(ctx context.Context, response string) (bool, map[string]interface{}) {
+	// Use universal parser to detect tool calls in any format
+	toolCalls := d.parseToolCalls(response)
+	if len(toolCalls) == 0 {
+		return false, nil // No tool calls found
+	}
+	
+	// Execute all tool calls and return results
+	results := d.executeToolCalls(ctx, toolCalls)
+	return true, results
 }
 
 func (d *Del) showHelp() {
@@ -976,6 +1446,8 @@ func (d *Del) showHelp() {
   clear, cls           Clear screen
   context, ctx         Show project context
   tools                List all available tools
+  safe                 Toggle safe mode (confirmation prompts)
+  unsafe               Disable safe mode (DANGEROUS)
 
 🔥 Power User Tips:
   Just talk naturally! Del understands:
@@ -989,6 +1461,7 @@ func (d *Del) showHelp() {
 
 🔧 Available Tools:
   ✓ File Operations     • read, write, list files
+  ✓ Multi-File Editing  • batch edits across files
   ✓ Code Analysis       • analyze, format, lint code
   ✓ Security Scanning   • find vulnerabilities
   ✓ Git Operations      • status, commit, push, diff, log
@@ -996,6 +1469,7 @@ func (d *Del) showHelp() {
   ✓ Test Running        • auto-detect and run project tests
   ✓ Command Execution   • run any shell command
   ✓ Project Understanding • context-aware assistance
+  🛡️ Safe Mode         • confirmation prompts for dangerous ops
 
 🎯 Pro Tip: Del learns your project as you work!
 `)
@@ -1007,12 +1481,17 @@ func (d *Del) clearScreen() {
 
 func (d *Del) showContext() {
 	cwd, _ := os.Getwd()
+	safeStatus := "🛡️  ENABLED"
+	if !d.safeMode {
+		safeStatus = "⚠️  DISABLED"
+	}
 	fmt.Printf(`
 💼 Current Project Context:
   📁 Directory: %s
   🤖 Model: %s
   💬 Chat History: %d messages
-`, cwd, d.aiProvider.Name(), len(d.chatHistory))
+  🛡️ Safe Mode: %s
+`, cwd, d.aiProvider.Name(), len(d.chatHistory), safeStatus)
 	
 	// Show project type detection
 	projectTypes := map[string]string{
@@ -1100,24 +1579,58 @@ func (d *Del) StartREPL(ctx context.Context) {
 		case "tools":
 			d.showTools()
 			continue
+		case "safe":
+			d.safeMode = !d.safeMode
+			if d.safeMode {
+				fmt.Println("🛡️  Safe mode ENABLED - dangerous operations will require confirmation")
+			} else {
+				fmt.Println("⚠️  Safe mode DISABLED - dangerous operations will execute without confirmation")
+			}
+			continue
+		case "unsafe":
+			d.safeMode = false
+			fmt.Println("⚠️  Safe mode DISABLED - BE CAREFUL!")
+			continue
 		}
 		
 		fmt.Print("🤖 Del: ")
 		d.chatHistory = append(d.chatHistory, api.Message{Role: "user", Content: input})
 		
-		// Use non-streaming for now to debug
+		// Stage 1: Get initial response from model (should contain tool calls)
 		response, err := d.aiProvider.Chat(ctx, d.chatHistory)
 		if err != nil {
 			fmt.Printf("\n⚠️  Error: %s\n\n", err)
 			continue
 		}
-		fmt.Print(response)
 		
-		d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: response})
-		output := d.handleToolCalls(ctx, response)
-		if output != "" {
-			fmt.Print(output)
+		// Check if there are tool calls to execute
+		hasTools, toolResults := d.handleToolCalls(ctx, response)
+		
+		if hasTools {
+			// Stage 2: Tool calls were found and executed, ask model for final response
+			d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: response})
+			
+			fmt.Printf("\n📋 Providing tool results to model for final response...\n")
+			
+			// Create tool results message
+			toolResultsJSON, _ := json.MarshalIndent(toolResults, "", "  ")
+			toolMessage := fmt.Sprintf("Tool execution results:\n```json\n%s\n```\n\nPlease provide a natural response based on these results.", string(toolResultsJSON))
+			d.chatHistory = append(d.chatHistory, api.Message{Role: "user", Content: toolMessage})
+			
+			// Get final response with tool results
+			finalResponse, err := d.aiProvider.Chat(ctx, d.chatHistory)
+			if err != nil {
+				fmt.Printf("\n⚠️  Error getting final response: %s\n\n", err)
+				continue
+			}
+			fmt.Printf("🤖 Del: %s", finalResponse)
+			d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: finalResponse})
+		} else {
+			// No tool calls, just display the response normally
+			fmt.Print(response)
+			d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: response})
 		}
+		
 		fmt.Println()
 	}
 }
go.sum
@@ -2,10 +2,10 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
 github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
-github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/ollama/ollama v0.9.2 h1:NEzeLb0gwz1XRyQUCPb30zqDyO/bze+Hiq9NUuYEUy4=
-github.com/ollama/ollama v0.9.2/go.mod h1:+5wt6UPgPmzYhnpLJ/rObxJJyEXURZ/SKKCMQsff8bA=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/ollama/ollama v0.9.2 h1:mN651uuzTx3Ct3QKUPNHZspqnrG/XlzxukNLCnMJMsk=
+github.com/ollama/ollama v0.9.2/go.mod h1:aio9yQ7nc4uwIbn6S0LkGEPgn8/9bNQLL1nHuH+OcD0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=