Commit 14c66d8
Changed files (1)
cmd
del
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