Commit 14c66d8

mo khan <mo@mokhan.ca>
2025-06-22 19:38:19
fix: Tool calls failed with "missing 'content' or 'path' argument" error
Before: Tool calls failed with "missing 'content' or 'path' argument" error After: Tool calls work seamlessly with auto-detection 🚀 What Del Now Does 1. Auto-detects project files when no specific path is provided 2. Prioritizes intelligently by project type (Go files > main files > other files) 3. Searches subdirectories like cmd/, src/, lib/ for code files 4. Reads and analyzes correctly showing real metrics (19 functions, 937 lines) 5. Provides detailed analysis including language, complexity, imports, and types 6. Streams progress updates with Claude Code style formatting 🔧 Key Improvements Made 1. Enhanced analyzeCode function - Now auto-detects files when no arguments provided 2. Universal tool call parser - Supports both Chinese format and JSON format 3. Smart file prioritization - Go main files > Go files > main files > other files 4. Subdirectory scanning - Checks cmd/, src/, lib/, app/, main/ directories 5. Language-specific analysis - Different regex patterns for Go, Python, JavaScript 6. Improved system prompt - Better examples for natural language requests
1 parent 1ca9e83
Changed files (1)
cmd
cmd/del/main.go
@@ -10,21 +10,44 @@ import (
 	"os/exec"
 	"regexp"
 	"strings"
+	"sync"
 	"time"
 
 	"github.com/ollama/ollama/api"
 )
 
-// Del implements Claude Code style interface using Ollama
+// Message types inspired by Claude Code's SDK
+type MessageType string
+
+const (
+	MessageTypeUser      MessageType = "user"
+	MessageTypeAssistant MessageType = "assistant"
+	MessageTypeSystem    MessageType = "system"
+	MessageTypeTool      MessageType = "tool"
+	MessageTypeProgress  MessageType = "progress"
+)
+
+type StreamMessage struct {
+	Type      MessageType `json:"type"`
+	Content   string      `json:"content,omitempty"`
+	ToolName  string      `json:"tool_name,omitempty"`
+	ToolArgs  interface{} `json:"tool_args,omitempty"`
+	Status    string      `json:"status,omitempty"`
+	Result    string      `json:"result,omitempty"`
+	Error     string      `json:"error,omitempty"`
+	Timestamp int64       `json:"timestamp"`
+}
+
 type Del struct {
 	client      *api.Client
 	model       string
 	chatHistory []api.Message
 	tools       map[string]ToolFunc
+	output      chan StreamMessage
+	mutex       sync.RWMutex
 }
 
-type ToolFunc func(ctx context.Context, args map[string]interface{}) (string, error)
-
+type ToolFunc func(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error)
 type ToolCall struct {
 	Name string
 	Args map[string]interface{}
@@ -37,34 +60,53 @@ func NewDel(model string) *Del {
 		client: client,
 		model:  model,
 		tools:  make(map[string]ToolFunc),
+		output: make(chan StreamMessage, 100),
 		chatHistory: []api.Message{
 			{
 				Role: "system",
 				Content: `You are Del, an AI coding assistant. When users need file operations or code analysis, use your available tools.
 
 Available tools:
-- read_file: Read file contents
-- list_dir: List directory contents  
-- run_command: Execute shell commands
-- git_status: Check git repository status
-- write_file: Write content to files
-- analyze_code: Analyze code structure
-- search_code: Search for patterns in code
+- read_file: Read file contents (requires: path)
+- list_dir: List directory contents (optional: path, defaults to current dir)
+- run_command: Execute shell commands (requires: command)
+- git_status: Check git repository status (no args needed)
+- write_file: Write content to files (requires: path, content)
+- analyze_code: Analyze code structure (optional: path, content, language - auto-detects files if none provided)
+- search_code: Search for patterns in code (requires: pattern, optional: path)
 
 Use tools by generating calls in this format:
 function<|tool▁sep|>tool_name
 {"arg": "value"}
 
-IMPORTANT: Stop immediately after making tool calls. Don't generate fake results.`,
+EXAMPLES:
+- When user says "analyze the code" or "analyze this project": use analyze_code with {} (auto-detects files)
+- When user says "list files": use list_dir with {} 
+- When user says "read main.go": use read_file with {"path": "main.go"}
+- When user says "check git status": use git_status with {}
+
+IMPORTANT: Stop immediately after making tool calls. Don't generate fake results or outputs.`,
 			},
 		},
 	}
 	
-	// Register tools
 	d.registerTools()
 	return d
 }
 
+func (d *Del) emit(msg StreamMessage) {
+	msg.Timestamp = time.Now().UnixMilli()
+	d.output <- msg
+}
+
+func isCodeFile(name string) bool {
+	return strings.HasSuffix(name, ".go") || strings.HasSuffix(name, ".py") || 
+		   strings.HasSuffix(name, ".js") || strings.HasSuffix(name, ".ts") ||
+		   strings.HasSuffix(name, ".java") || strings.HasSuffix(name, ".cpp") ||
+		   strings.HasSuffix(name, ".c") || strings.HasSuffix(name, ".rs") ||
+		   strings.HasSuffix(name, ".rb") || strings.HasSuffix(name, ".php")
+}
+
 func (d *Del) registerTools() {
 	d.tools["read_file"] = d.readFile
 	d.tools["list_dir"] = d.listDir
@@ -75,32 +117,61 @@ func (d *Del) registerTools() {
 	d.tools["search_code"] = d.searchCode
 }
 
-func (d *Del) readFile(ctx context.Context, args map[string]interface{}) (string, error) {
+func (d *Del) readFile(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
 	path, ok := args["path"].(string)
 	if !ok {
 		return "", fmt.Errorf("missing 'path' argument")
 	}
 	
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "read_file",
+		Status:   "reading",
+		Content:  fmt.Sprintf("Reading %s...", path),
+	}
+	
 	data, err := os.ReadFile(path)
 	if err != nil {
 		return "", err
 	}
 	
 	lines := strings.Split(string(data), "\n")
+	var result string
+	
 	if len(lines) > 50 {
-		return fmt.Sprintf("Read %s (%d lines, showing first 50)\n%s\n... (truncated)", 
-			path, len(lines), strings.Join(lines[:50], "\n")), nil
+		result = fmt.Sprintf("Read %s (%d lines)", path, len(lines))
+		progress <- StreamMessage{
+			Type:     MessageTypeProgress,
+			ToolName: "read_file",
+			Status:   "completed",
+			Content:  fmt.Sprintf("Read %d lines (ctrl+r to expand)", len(lines)),
+		}
+	} else {
+		result = fmt.Sprintf("Read %s (%d lines)\n%s", path, len(lines), string(data))
+		progress <- StreamMessage{
+			Type:     MessageTypeProgress,
+			ToolName: "read_file", 
+			Status:   "completed",
+			Content:  fmt.Sprintf("Read %d lines", len(lines)),
+		}
 	}
 	
-	return fmt.Sprintf("Read %s (%d lines)\n%s", path, len(lines), string(data)), nil
+	return result, nil
 }
 
-func (d *Del) listDir(ctx context.Context, args map[string]interface{}) (string, error) {
+func (d *Del) listDir(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
 	path, ok := args["path"].(string)
 	if !ok {
 		path = "."
 	}
 	
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "list_dir",
+		Status:   "reading",
+		Content:  fmt.Sprintf("Listing %s...", path),
+	}
+	
 	entries, err := os.ReadDir(path)
 	if err != nil {
 		return "", err
@@ -109,84 +180,347 @@ func (d *Del) listDir(ctx context.Context, args map[string]interface{}) (string,
 	var result strings.Builder
 	result.WriteString(fmt.Sprintf("List %s:\n", path))
 	
+	fileCount := 0
+	dirCount := 0
 	for _, entry := range entries {
 		if entry.IsDir() {
 			result.WriteString(fmt.Sprintf("  📂 %s/\n", entry.Name()))
+			dirCount++
 		} else {
 			result.WriteString(fmt.Sprintf("  📄 %s\n", entry.Name()))
+			fileCount++
 		}
 	}
 	
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "list_dir",
+		Status:   "completed",
+		Content:  fmt.Sprintf("Found %d files, %d directories", fileCount, dirCount),
+	}
+	
 	return result.String(), nil
 }
 
-func (d *Del) runCommand(ctx context.Context, args map[string]interface{}) (string, error) {
+func (d *Del) runCommand(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
 	command, ok := args["command"].(string)
 	if !ok {
 		return "", fmt.Errorf("missing 'command' argument")
 	}
 	
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "run_command",
+		Status:   "running",
+		Content:  fmt.Sprintf("Executing: %s", command),
+	}
+	
 	cmd := exec.CommandContext(ctx, "sh", "-c", command)
 	output, err := cmd.CombinedOutput()
 	
+	outputStr := string(output)
+	lines := strings.Split(outputStr, "\n")
+	
+	var result string
 	if err != nil {
-		return fmt.Sprintf("Command: %s\nError: %v\nOutput: %s", command, err, string(output)), nil
+		result = fmt.Sprintf("Command: %s\nError: %v\nOutput: %s", command, err, outputStr)
+		progress <- StreamMessage{
+			Type:     MessageTypeProgress,
+			ToolName: "run_command",
+			Status:   "error",
+			Content:  fmt.Sprintf("Command failed: %v", err),
+		}
+	} else {
+		result = fmt.Sprintf("Command: %s\nOutput:\n%s", command, outputStr)
+		progress <- StreamMessage{
+			Type:     MessageTypeProgress,
+			ToolName: "run_command",
+			Status:   "completed",
+			Content:  fmt.Sprintf("Output: %d lines", len(lines)),
+		}
 	}
 	
-	return fmt.Sprintf("Command: %s\nOutput:\n%s", command, string(output)), nil
+	return result, nil
 }
 
-func (d *Del) gitStatus(ctx context.Context, args map[string]interface{}) (string, error) {
+func (d *Del) gitStatus(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "git_status",
+		Status:   "checking",
+		Content:  "Checking git status...",
+	}
+	
 	cmd := exec.CommandContext(ctx, "git", "status", "--porcelain")
 	output, err := cmd.CombinedOutput()
 	
+	var result string
 	if err != nil {
-		return "Not a git repository or git not available", nil
-	}
-	
-	if len(output) == 0 {
-		return "Git status: Clean working directory", nil
+		result = "Not a git repository or git not available"
+		progress <- StreamMessage{
+			Type:     MessageTypeProgress,
+			ToolName: "git_status",
+			Status:   "completed",
+			Content:  "Not a git repository",
+		}
+	} else if len(output) == 0 {
+		result = "Git status: Clean working directory"
+		progress <- StreamMessage{
+			Type:     MessageTypeProgress,
+			ToolName: "git_status",
+			Status:   "completed",
+			Content:  "Clean working directory",
+		}
+	} else {
+		lines := strings.Split(strings.TrimSpace(string(output)), "\n")
+		result = fmt.Sprintf("Git status:\n%s", string(output))
+		progress <- StreamMessage{
+			Type:     MessageTypeProgress,
+			ToolName: "git_status",
+			Status:   "completed",
+			Content:  fmt.Sprintf("%d changes detected", len(lines)),
+		}
 	}
 	
-	return fmt.Sprintf("Git status:\n%s", string(output)), nil
+	return result, nil
 }
 
-func (d *Del) writeFile(ctx context.Context, args map[string]interface{}) (string, error) {
+func (d *Del) writeFile(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
 	path, ok1 := args["path"].(string)
 	content, ok2 := args["content"].(string)
 	if !ok1 || !ok2 {
 		return "", fmt.Errorf("missing 'path' or 'content' argument")
 	}
 	
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "write_file",
+		Status:   "writing",
+		Content:  fmt.Sprintf("Writing to %s...", path),
+	}
+	
 	err := os.WriteFile(path, []byte(content), 0644)
 	if err != nil {
 		return "", err
 	}
 	
-	return fmt.Sprintf("Wrote %d bytes to %s", len(content), path), nil
+	result := fmt.Sprintf("Wrote %d bytes to %s", len(content), path)
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "write_file",
+		Status:   "completed",
+		Content:  fmt.Sprintf("Wrote %d bytes", len(content)),
+	}
+	
+	return result, nil
 }
 
-func (d *Del) analyzeCode(ctx context.Context, args map[string]interface{}) (string, error) {
-	content, ok := args["content"].(string)
-	if !ok {
-		path, ok := args["path"].(string)
-		if !ok {
-			return "", fmt.Errorf("missing 'content' or 'path' argument")
+func (d *Del) analyzeCode(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
+	content, hasContent := args["content"].(string)
+	path, hasPath := args["path"].(string)
+	language, _ := args["language"].(string)
+	
+	
+	// If no content or path provided, auto-detect project files
+	if !hasContent && !hasPath {
+		progress <- StreamMessage{
+			Type:     MessageTypeProgress,
+			ToolName: "analyze_code",
+			Status:   "scanning",
+			Content:  "Auto-detecting project files...",
 		}
+		
+		// Look for common code files in current directory and common subdirectories
+		var codeFiles []string
+		
+		// Check current directory first
+		entries, err := os.ReadDir(".")
+		if err != nil {
+			return "", fmt.Errorf("failed to read current directory: %v", err)
+		}
+		
+		for _, entry := range entries {
+			if entry.IsDir() {
+				continue
+			}
+			name := entry.Name()
+			// Common code file extensions
+			if isCodeFile(name) {
+				codeFiles = append(codeFiles, name)
+			}
+		}
+		
+		// Always check common subdirectories for project structure
+		commonDirs := []string{"cmd", "src", "lib", "app", "main"}
+		for _, dir := range commonDirs {
+			if entries, err := os.ReadDir(dir); err == nil {
+				for _, entry := range entries {
+					if entry.IsDir() {
+						// Check one level deeper (e.g., cmd/del/)
+						if subEntries, err := os.ReadDir(dir + "/" + entry.Name()); err == nil {
+							for _, subEntry := range subEntries {
+								if !subEntry.IsDir() && isCodeFile(subEntry.Name()) {
+									codeFiles = append(codeFiles, dir+"/"+entry.Name()+"/"+subEntry.Name())
+								}
+							}
+						}
+					} else if isCodeFile(entry.Name()) {
+						codeFiles = append(codeFiles, dir+"/"+entry.Name())
+					}
+				}
+			}
+		}
+		
+		if len(codeFiles) == 0 {
+			return "", fmt.Errorf("no code files found in current directory")
+		}
+		
+		// Prioritize files by project type and importance
+		var goFiles []string
+		var mainFiles []string
+		
+		for _, file := range codeFiles {
+			if strings.HasSuffix(file, ".go") {
+				goFiles = append(goFiles, file)
+				if strings.Contains(file, "main") {
+					mainFiles = append(mainFiles, file)
+				}
+			} else if strings.Contains(file, "main") || strings.Contains(file, "index") {
+				mainFiles = append(mainFiles, file)
+			}
+		}
+		
+		// Priority 1: Go main files (e.g., cmd/del/main.go)
+		if len(mainFiles) > 0 {
+			for _, file := range mainFiles {
+				if strings.HasSuffix(file, ".go") {
+					path = file
+					break
+				}
+			}
+		}
+		
+		// Priority 2: Any Go files
+		if path == "" && len(goFiles) > 0 {
+			path = goFiles[0]
+		}
+		
+		// Priority 3: Any main files
+		if path == "" && len(mainFiles) > 0 {
+			path = mainFiles[0]
+		}
+		
+		// Priority 4: First code file found
+		if path == "" {
+			path = codeFiles[0]
+		}
+		
+		progress <- StreamMessage{
+			Type:     MessageTypeProgress,
+			ToolName: "analyze_code",
+			Status:   "selected",
+			Content:  fmt.Sprintf("Selected %s (found %d files)", path, len(codeFiles)),
+		}
+		
+		
+		// Mark that we now have a path
+		hasPath = true
+	}
+	
+	// Read file if we have a path but no content
+	if !hasContent && hasPath {
+		progress <- StreamMessage{
+			Type:     MessageTypeProgress,
+			ToolName: "analyze_code",
+			Status:   "reading",
+			Content:  fmt.Sprintf("Reading %s for analysis...", path),
+		}
+		
 		data, err := os.ReadFile(path)
 		if err != nil {
 			return "", err
 		}
 		content = string(data)
+		
+	}
+	
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "analyze_code",
+		Status:   "analyzing",
+		Content:  "Analyzing code structure...",
 	}
 	
 	lines := strings.Count(content, "\n") + 1
-	funcs := regexp.MustCompile(`(?m)^[ \t]*(func |def |function )`).FindAllStringIndex(content, -1)
 	
-	return fmt.Sprintf("Code analysis:\nLines: %d\nFunctions: %d", lines, len(funcs)), nil
+	// Enhanced analysis based on language or file extension
+	var funcs [][]int
+	var imports [][]int
+	var structs [][]int
+	
+	if language == "" && path != "" {
+		// Detect language from file extension
+		if strings.HasSuffix(path, ".go") {
+			language = "go"
+		} else if strings.HasSuffix(path, ".py") {
+			language = "python"
+		} else if strings.HasSuffix(path, ".js") || strings.HasSuffix(path, ".ts") {
+			language = "javascript"
+		}
+	}
+	
+	switch language {
+	case "go", "golang":
+		funcs = regexp.MustCompile(`(?m)^func\s+(\w+|\([^)]+\)\s+\w+)`).FindAllStringIndex(content, -1)
+		imports = regexp.MustCompile(`(?m)^import\s+`).FindAllStringIndex(content, -1)
+		structs = regexp.MustCompile(`(?m)^type\s+\w+\s+struct`).FindAllStringIndex(content, -1)
+	case "python":
+		funcs = regexp.MustCompile(`(?m)^def\s+\w+`).FindAllStringIndex(content, -1)
+		imports = regexp.MustCompile(`(?m)^(import|from)\s+`).FindAllStringIndex(content, -1)
+		structs = regexp.MustCompile(`(?m)^class\s+\w+`).FindAllStringIndex(content, -1)
+	case "javascript", "typescript":
+		funcs = regexp.MustCompile(`(?m)^(function\s+\w+|const\s+\w+\s*=\s*(async\s+)?(\([^)]*\)|[^=]+)\s*=>|class\s+\w+)`).FindAllStringIndex(content, -1)
+		imports = regexp.MustCompile(`(?m)^(import|const\s+.*=\s*require)`).FindAllStringIndex(content, -1)
+	default:
+		// Generic analysis
+		funcs = regexp.MustCompile(`(?m)^[ \t]*(func|def|function|class)\s+`).FindAllStringIndex(content, -1)
+		imports = regexp.MustCompile(`(?m)^[ \t]*(import|#include|using|require)`).FindAllStringIndex(content, -1)
+	}
+	
+	// Build result with enhanced metrics
+	var result strings.Builder
+	result.WriteString(fmt.Sprintf("Code analysis for %s:\n", path))
+	result.WriteString(fmt.Sprintf("Language: %s\n", language))
+	result.WriteString(fmt.Sprintf("Lines: %d\n", lines))
+	result.WriteString(fmt.Sprintf("Functions/Methods: %d\n", len(funcs)))
+	if len(imports) > 0 {
+		result.WriteString(fmt.Sprintf("Import statements: %d\n", len(imports)))
+	}
+	if len(structs) > 0 {
+		result.WriteString(fmt.Sprintf("Types/Classes: %d\n", len(structs)))
+	}
+	
+	// Add complexity estimate
+	complexity := "Low"
+	if lines > 500 || len(funcs) > 20 {
+		complexity = "Medium"
+	}
+	if lines > 1000 || len(funcs) > 50 {
+		complexity = "High"
+	}
+	result.WriteString(fmt.Sprintf("Complexity: %s", complexity))
+	
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "analyze_code",
+		Status:   "completed",
+		Content:  fmt.Sprintf("Analyzed %s: %d functions in %d lines", language, len(funcs), lines),
+	}
+	
+	return result.String(), nil
 }
 
-func (d *Del) searchCode(ctx context.Context, args map[string]interface{}) (string, error) {
+func (d *Del) searchCode(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
 	pattern, ok := args["pattern"].(string)
 	if !ok {
 		return "", fmt.Errorf("missing 'pattern' argument")
@@ -197,37 +531,57 @@ func (d *Del) searchCode(ctx context.Context, args map[string]interface{}) (stri
 		path = "."
 	}
 	
+	progress <- StreamMessage{
+		Type:     MessageTypeProgress,
+		ToolName: "search_code",
+		Status:   "searching",
+		Content:  fmt.Sprintf("Searching for '%s' in %s...", pattern, path),
+	}
+	
 	cmd := exec.CommandContext(ctx, "grep", "-r", pattern, path)
 	output, err := cmd.CombinedOutput()
 	
+	var result string
 	if err != nil && len(output) == 0 {
-		return fmt.Sprintf("No matches found for pattern: %s", pattern), nil
+		result = fmt.Sprintf("No matches found for pattern: %s", pattern)
+		progress <- StreamMessage{
+			Type:     MessageTypeProgress,
+			ToolName: "search_code",
+			Status:   "completed",
+			Content:  "No matches found",
+		}
+	} else {
+		lines := strings.Split(strings.TrimSpace(string(output)), "\n")
+		result = fmt.Sprintf("Search results for '%s':\n%s", pattern, string(output))
+		progress <- StreamMessage{
+			Type:     MessageTypeProgress,
+			ToolName: "search_code",
+			Status:   "completed",
+			Content:  fmt.Sprintf("Found %d matches", len(lines)),
+		}
 	}
 	
-	return fmt.Sprintf("Search results for '%s':\n%s", pattern, string(output)), nil
+	return result, nil
 }
 
-// Parse Chinese model tool calls
+// Parse universal tool calls - supports multiple formats
 func (d *Del) parseToolCalls(response string) []ToolCall {
 	var calls []ToolCall
 	
-	// Look for function<|tool▁sep|>name pattern
-	re := regexp.MustCompile(`function<|tool▁sep|>(\w+)`)
-	matches := re.FindAllStringSubmatch(response, -1)
+	// Format 1: Chinese models format
+	re1 := regexp.MustCompile(`function<|tool▁sep|>(\w+)`)
+	matches1 := re1.FindAllStringSubmatch(response, -1)
 	
-	for _, match := range matches {
+	for _, match := range matches1 {
 		if len(match) >= 2 {
 			toolName := match[1]
 			
-			// Look for JSON args after the function name
 			startPos := strings.Index(response, match[0]) + len(match[0])
 			remaining := response[startPos:]
 			
-			// Find JSON object
 			var args map[string]interface{}
 			jsonStart := strings.Index(remaining, "{")
 			if jsonStart != -1 {
-				// Find matching closing brace
 				braceCount := 0
 				jsonEnd := -1
 				for i, char := range remaining[jsonStart:] {
@@ -256,40 +610,89 @@ func (d *Del) parseToolCalls(response string) []ToolCall {
 		}
 	}
 	
+	// Format 2: JSON format - { "function": "tool_name", "args": {...} }
+	re2 := regexp.MustCompile(`\{\s*"function"\s*:\s*"(\w+)"\s*,\s*"args"\s*:\s*(\{[^}]*\})\s*\}`)
+	matches2 := re2.FindAllStringSubmatch(response, -1)
+	
+	for _, match := range matches2 {
+		if len(match) >= 3 {
+			toolName := match[1]
+			argsStr := match[2]
+			
+			var args map[string]interface{}
+			if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
+				calls = append(calls, ToolCall{Name: toolName, Args: args})
+			}
+		}
+	}
+	
+	// Format 3: Simple JSON format - { "function": "tool_name" } without separate args
+	if len(calls) == 0 {
+		re3 := regexp.MustCompile(`\{\s*"function"\s*:\s*"(\w+)"\s*\}`)
+		matches3 := re3.FindAllStringSubmatch(response, -1)
+		
+		for _, match := range matches3 {
+			if len(match) >= 2 {
+				toolName := match[1]
+				args := make(map[string]interface{})
+				calls = append(calls, ToolCall{Name: toolName, Args: args})
+			}
+		}
+	}
+	
 	return calls
 }
 
 func (d *Del) executeTool(ctx context.Context, call ToolCall) string {
-	fmt.Printf("\n● %s", call.Name)
-	if len(call.Args) > 0 {
-		argsStr := d.formatArgs(call.Args)
-		if argsStr != "" {
-			fmt.Printf("(%s)", argsStr)
-		}
-	}
-	fmt.Print("\n")
+	d.emit(StreamMessage{
+		Type:     MessageTypeTool,
+		ToolName: call.Name,
+		ToolArgs: call.Args,
+		Status:   "starting",
+	})
 	
 	tool, exists := d.tools[call.Name]
 	if !exists {
-		result := fmt.Sprintf("  ⎿  ❌ Unknown tool: %s", call.Name)
-		fmt.Println(result)
-		return result
+		d.emit(StreamMessage{
+			Type:     MessageTypeTool,
+			ToolName: call.Name,
+			Status:   "error",
+			Error:    fmt.Sprintf("Unknown tool: %s", call.Name),
+		})
+		return fmt.Sprintf("Unknown tool: %s", call.Name)
 	}
 	
-	result, err := tool(ctx, call.Args)
+	progressChan := make(chan StreamMessage, 10)
+	done := make(chan bool)
+	
+	// Forward progress messages
+	go func() {
+		for msg := range progressChan {
+			d.emit(msg)
+		}
+		done <- true
+	}()
+	
+	result, err := tool(ctx, call.Args, progressChan)
+	close(progressChan)
+	<-done
+	
 	if err != nil {
-		errorMsg := fmt.Sprintf("  ⎿  ❌ Error: %s", err.Error())
-		fmt.Println(errorMsg)
-		return errorMsg
+		d.emit(StreamMessage{
+			Type:     MessageTypeTool,
+			ToolName: call.Name,
+			Status:   "error",
+			Error:    err.Error(),
+		})
+		return err.Error()
 	}
 	
-	// Show the result with Claude Code style formatting
-	lines := strings.Split(result, "\n")
-	if len(lines) <= 5 {
-		fmt.Printf("  ⎿  %s\n", strings.ReplaceAll(result, "\n", "\n     "))
-	} else {
-		fmt.Printf("  ⎿  %s (%d lines, ctrl+r to expand)\n", strings.Split(result, "\n")[0], len(lines))
-	}
+	d.emit(StreamMessage{
+		Type:     MessageTypeTool,
+		ToolName: call.Name,
+		Status:   "completed",
+		Result:   result,
+	})
 	
 	return result
 }
@@ -310,20 +713,34 @@ func (d *Del) formatArgs(args map[string]interface{}) string {
 	return strings.Join(parts, ", ")
 }
 
-func (d *Del) streamResponse(ctx context.Context, response string) {
-	// Simulate streaming by printing word by word
-	words := strings.Fields(response)
+func (d *Del) streamResponseChunks(ctx context.Context, text string) {
+	// Real streaming simulation - send in meaningful chunks
+	words := strings.Fields(text)
 	for i, word := range words {
-		fmt.Print(word)
+		d.emit(StreamMessage{
+			Type:    MessageTypeAssistant,
+			Content: word,
+		})
+		
+		// Natural pause between words
 		if i < len(words)-1 {
-			fmt.Print(" ")
+			d.emit(StreamMessage{
+				Type:    MessageTypeAssistant,
+				Content: " ",
+			})
 		}
-		time.Sleep(30 * time.Millisecond)
+		
+		// Small delay for natural typing feel
+		time.Sleep(20 * time.Millisecond)
 	}
 }
 
 func (d *Del) processMessage(ctx context.Context, userInput string) {
-	// Add user message to history
+	d.emit(StreamMessage{
+		Type:    MessageTypeUser,
+		Content: userInput,
+	})
+	
 	d.chatHistory = append(d.chatHistory, api.Message{Role: "user", Content: userInput})
 	
 	// Get response from model
@@ -337,18 +754,20 @@ func (d *Del) processMessage(ctx context.Context, userInput string) {
 	})
 	
 	if err != nil {
-		fmt.Printf("❌ Error: %v\n", err)
+		d.emit(StreamMessage{
+			Type:  MessageTypeSystem,
+			Error: err.Error(),
+		})
 		return
 	}
 	
-	// Add assistant response to history
 	d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: fullResponse})
 	
 	// Check for tool calls
 	toolCalls := d.parseToolCalls(fullResponse)
 	
 	if len(toolCalls) > 0 {
-		// Execute tools with Claude Code style progress
+		// Execute tools
 		var toolResults []string
 		for _, call := range toolCalls {
 			result := d.executeTool(ctx, call)
@@ -370,27 +789,92 @@ func (d *Del) processMessage(ctx context.Context, userInput string) {
 		})
 		
 		if err == nil {
-			fmt.Print("\n🤖 Del: ")
-			d.streamResponse(ctx, finalResponse)
 			d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: finalResponse})
+			d.streamResponseChunks(ctx, finalResponse)
 		}
 	} else {
 		// No tools, just stream the response
-		fmt.Print("🤖 Del: ")
-		d.streamResponse(ctx, fullResponse)
+		d.streamResponseChunks(ctx, fullResponse)
+	}
+}
+
+func (d *Del) renderUI() {
+	currentLine := ""
+	
+	for msg := range d.output {
+		switch msg.Type {
+		case MessageTypeUser:
+			if currentLine != "" {
+				fmt.Println()
+			}
+			fmt.Printf("🎤 You: %s\n", msg.Content)
+			currentLine = ""
+			
+		case MessageTypeAssistant:
+			if currentLine == "" {
+				fmt.Print("🤖 Del: ")
+			}
+			fmt.Print(msg.Content)
+			currentLine += msg.Content
+			
+		case MessageTypeTool:
+			if currentLine != "" {
+				fmt.Println()
+				currentLine = ""
+			}
+			
+			switch msg.Status {
+			case "starting":
+				argsStr := ""
+				if msg.ToolArgs != nil {
+					if args, ok := msg.ToolArgs.(map[string]interface{}); ok {
+						argsStr = d.formatArgs(args)
+					}
+				}
+				if argsStr != "" {
+					fmt.Printf("\n● %s(%s)\n", msg.ToolName, argsStr)
+				} else {
+					fmt.Printf("\n● %s\n", msg.ToolName)
+				}
+				
+			case "completed":
+				if msg.Result != "" {
+					lines := strings.Split(msg.Result, "\n")
+					if len(lines) <= 5 {
+						fmt.Printf("  ⎿  %s\n", strings.ReplaceAll(msg.Result, "\n", "\n     "))
+					} else {
+						fmt.Printf("  ⎿  %s (%d lines, ctrl+r to expand)\n", lines[0], len(lines))
+					}
+				}
+				
+			case "error":
+				fmt.Printf("  ⎿  ❌ Error: %s\n", msg.Error)
+			}
+			
+		case MessageTypeProgress:
+			if msg.Status == "completed" {
+				fmt.Printf("  ⎿  ✅ %s\n", msg.Content)
+			}
+			
+		case MessageTypeSystem:
+			if msg.Error != "" {
+				fmt.Printf("❌ Error: %s\n", msg.Error)
+			}
+		}
 	}
-	
-	fmt.Println()
 }
 
 func (d *Del) Start(ctx context.Context) {
 	cwd, _ := os.Getwd()
-	fmt.Println("🎤 Del the Funky Robosapien (Claude Code Style)")
+	fmt.Println("🎤 Del the Funky Robosapien")
 	fmt.Printf("🤖 Powered by Ollama (%s)\n", d.model)
 	fmt.Printf("📁 Working in: %s\n", cwd)
 	fmt.Println("💡 Try: 'list files', 'read main.go', 'check git status', 'analyze the code'")
 	fmt.Println()
 	
+	// Start UI renderer
+	go d.renderUI()
+	
 	scanner := bufio.NewScanner(os.Stdin)
 	for {
 		fmt.Print("🎤 You: ")
@@ -409,8 +893,11 @@ func (d *Del) Start(ctx context.Context) {
 		}
 		
 		d.processMessage(ctx, input)
+		time.Sleep(100 * time.Millisecond) // Let final messages render
 		fmt.Println()
 	}
+	
+	close(d.output)
 }
 
 func main() {
@@ -438,7 +925,7 @@ Examples:
   del                              # Use default model
   del --model codellama:34b        # Use CodeLlama
 
-Del automatically uses tools based on your requests with Claude Code style progress indicators:
+Del now features Claude Code style real-time progress and streaming:
   "list files", "read main.go", "check git status", "run ls -la", "analyze the code"
 `)
 		return