Commit 1654fe4
Changed files (1)
cmd
del
cmd/del/main.go
@@ -48,6 +48,9 @@ type Del struct {
tools map[string]ToolFunc
output chan StreamMessage
mutex sync.RWMutex
+ thinking bool
+ thinkingMsg string
+ startTime time.Time
}
type ToolFunc func(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error)
@@ -118,6 +121,60 @@ func (d *Del) emit(msg StreamMessage) {
d.output <- msg
}
+func (d *Del) startThinking(message string) {
+ d.mutex.Lock()
+ d.thinking = true
+ d.thinkingMsg = message
+ d.startTime = time.Now()
+ d.mutex.Unlock()
+
+ d.emit(StreamMessage{
+ Type: "thinking",
+ Content: message,
+ Status: "start",
+ })
+}
+
+func (d *Del) stopThinking() {
+ d.mutex.Lock()
+ elapsed := time.Since(d.startTime)
+ d.thinking = false
+ d.thinkingMsg = ""
+ d.mutex.Unlock()
+
+ // Format timing nicely
+ var timeStr string
+ if elapsed < time.Millisecond {
+ timeStr = fmt.Sprintf("%.1fμs", float64(elapsed.Nanoseconds())/1000)
+ } else if elapsed < time.Second {
+ timeStr = fmt.Sprintf("%.1fms", float64(elapsed.Nanoseconds())/1000000)
+ } else {
+ timeStr = fmt.Sprintf("%.2fs", elapsed.Seconds())
+ }
+
+ d.emit(StreamMessage{
+ Type: "thinking",
+ Status: "stop",
+ Content: timeStr,
+ })
+}
+
+func (d *Del) updateThinking(message string) {
+ d.mutex.Lock()
+ if d.thinking {
+ d.thinkingMsg = message
+ d.mutex.Unlock()
+
+ d.emit(StreamMessage{
+ Type: "thinking",
+ Content: message,
+ Status: "update",
+ })
+ } else {
+ d.mutex.Unlock()
+ }
+}
+
func isCodeFile(name string) bool {
return strings.HasSuffix(name, ".go") || strings.HasSuffix(name, ".py") ||
strings.HasSuffix(name, ".js") || strings.HasSuffix(name, ".ts") ||
@@ -717,6 +774,25 @@ func (d *Del) editFile(ctx context.Context, args map[string]interface{}, progres
return "", fmt.Errorf("missing required arguments: file_path, old_string, new_string")
}
+ // Validate file path
+ if !filepath.IsAbs(filePath) {
+ return "", fmt.Errorf("file_path must be absolute, got: %s", filePath)
+ }
+
+ // Check if file exists
+ if _, err := os.Stat(filePath); os.IsNotExist(err) {
+ return "", fmt.Errorf("file does not exist: %s", filePath)
+ }
+
+ // Validate strings are not empty
+ if oldString == "" {
+ return "", fmt.Errorf("old_string cannot be empty")
+ }
+
+ if oldString == newString {
+ return "", fmt.Errorf("old_string and new_string are identical - no changes needed")
+ }
+
progress <- StreamMessage{
Type: MessageTypeProgress,
ToolName: "edit_file",
@@ -726,7 +802,7 @@ func (d *Del) editFile(ctx context.Context, args map[string]interface{}, progres
data, err := os.ReadFile(filePath)
if err != nil {
- return "", err
+ return "", fmt.Errorf("failed to read file: %v", err)
}
content := string(data)
@@ -878,11 +954,24 @@ func (d *Del) globFiles(ctx context.Context, args map[string]interface{}, progre
return nil // Skip errors
}
- // Convert ** pattern to simple matching
- simplePattern := strings.ReplaceAll(pattern, "**", "*")
- matched, _ := filepath.Match(simplePattern, filepath.Base(path))
- if matched {
- matches = append(matches, path)
+ if info.IsDir() {
+ return nil // Skip directories
+ }
+
+ // For **/*.ext patterns, match the extension
+ if strings.HasPrefix(pattern, "**/") {
+ suffix := strings.TrimPrefix(pattern, "**/")
+ matched, _ := filepath.Match(suffix, filepath.Base(path))
+ if matched {
+ matches = append(matches, path)
+ }
+ } else {
+ // For other ** patterns, use simple matching
+ simplePattern := strings.ReplaceAll(pattern, "**", "*")
+ matched, _ := filepath.Match(simplePattern, filepath.Base(path))
+ if matched {
+ matches = append(matches, path)
+ }
}
return nil
@@ -1292,12 +1381,21 @@ func (d *Del) webFetch(ctx context.Context, args map[string]interface{}, progres
Content: fmt.Sprintf("Fetching %s...", url),
}
- resp, err := http.Get(url)
+ // Create HTTP client with timeout
+ client := &http.Client{
+ Timeout: 10 * time.Second,
+ }
+
+ resp, err := client.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return "", fmt.Errorf("HTTP error: %d %s", resp.StatusCode, resp.Status)
+ }
+
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
@@ -1349,6 +1447,8 @@ func (d *Del) webSearch(ctx context.Context, args map[string]interface{}, progre
func (d *Del) executeTool(ctx context.Context, call ToolCall) string {
+ startTime := time.Now()
+
d.emit(StreamMessage{
Type: MessageTypeTool,
ToolName: call.Name,
@@ -1382,12 +1482,24 @@ func (d *Del) executeTool(ctx context.Context, call ToolCall) string {
close(progressChan)
<-done
+ elapsed := time.Since(startTime)
+
+ // Format timing nicely for tools
+ var timeStr string
+ if elapsed < time.Millisecond {
+ timeStr = fmt.Sprintf("%.1fμs", float64(elapsed.Nanoseconds())/1000)
+ } else if elapsed < time.Second {
+ timeStr = fmt.Sprintf("%.1fms", float64(elapsed.Nanoseconds())/1000000)
+ } else {
+ timeStr = fmt.Sprintf("%.2fs", elapsed.Seconds())
+ }
+
if err != nil {
d.emit(StreamMessage{
Type: MessageTypeTool,
ToolName: call.Name,
Status: "error",
- Error: err.Error(),
+ Error: fmt.Sprintf("%s (took %s)", err.Error(), timeStr),
})
return err.Error()
}
@@ -1397,6 +1509,7 @@ func (d *Del) executeTool(ctx context.Context, call ToolCall) string {
ToolName: call.Name,
Status: "completed",
Result: result,
+ Content: timeStr, // Store formatted timing in Content field
})
return result
@@ -1972,116 +2085,77 @@ func (d *Del) processMessage(ctx context.Context, userInput string) {
d.chatHistory = append(d.chatHistory, api.Message{Role: "user", Content: userInput})
- // Define tools for Ollama native tool calling
- tools := d.buildOllamaTools()
-
- // Try with native tools first, fall back if not supported
- var fullResponse string
- var assistantMessage api.Message
- var toolsSupported = true
-
- err := d.client.Chat(ctx, &api.ChatRequest{
- Model: d.model,
- Messages: d.chatHistory,
- Tools: tools,
- }, func(resp api.ChatResponse) error {
- fullResponse += resp.Message.Content
- assistantMessage = resp.Message
- return nil
- })
+ // Start thinking indicator
+ d.startThinking("🤔 Analyzing your request...")
- // If tools aren't supported, try without tools
- if err != nil && strings.Contains(err.Error(), "does not support tools") {
- toolsSupported = false
- fullResponse = ""
- err = d.client.Chat(ctx, &api.ChatRequest{
- Model: d.model,
- Messages: d.chatHistory,
- }, func(resp api.ChatResponse) error {
- fullResponse += resp.Message.Content
- assistantMessage = resp.Message
- return nil
- })
- }
-
- if err != nil {
- d.emit(StreamMessage{
- Type: MessageTypeSystem,
- Error: err.Error(),
- })
- return
- }
+ // For now, let's use simple fallback parsing to avoid model hanging issues
+ d.updateThinking("🔧 Executing tools (fallback mode)...")
- // Add assistant message to history
- d.chatHistory = append(d.chatHistory, assistantMessage)
-
- // Check for native tool calls from Ollama (only if tools are supported)
- if toolsSupported && len(assistantMessage.ToolCalls) > 0 {
- // Execute tools using native Ollama tool calls
- for _, apiCall := range assistantMessage.ToolCalls {
- // Convert api.ToolCall to our internal format
- call := ToolCall{
- Name: apiCall.Function.Name,
- Args: apiCall.Function.Arguments,
- }
-
- // Execute the tool
+ // Parse user input for tool calls
+ toolCalls := d.parseTextToolCalls(userInput)
+ if len(toolCalls) > 0 {
+ // Execute any tools found in user input
+ for _, call := range toolCalls {
+ d.updateThinking(fmt.Sprintf("⚡ Running %s...", call.Name))
result := d.executeTool(ctx, call)
- // Add tool result as a 'tool' role message
+ // Add tool result as context for next message
d.chatHistory = append(d.chatHistory, api.Message{
- Role: "tool",
- Content: result,
+ Role: "user",
+ Content: fmt.Sprintf("Tool result for %s: %s", call.Name, result),
})
}
- // Get final response with tool results
+ d.updateThinking("🎨 Generating final response...")
+
+ // Get final response with tool results (with timeout)
var finalResponse string
- err = d.client.Chat(ctx, &api.ChatRequest{
+ chatCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
+ defer cancel()
+
+ err := d.client.Chat(chatCtx, &api.ChatRequest{
Model: d.model,
Messages: d.chatHistory,
- Tools: tools,
}, func(resp api.ChatResponse) error {
finalResponse += resp.Message.Content
return nil
})
+ d.stopThinking()
if err == nil {
d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: finalResponse})
d.streamResponseChunks(ctx, finalResponse)
+ } else {
+ d.emit(StreamMessage{
+ Type: MessageTypeSystem,
+ Error: err.Error(),
+ })
}
} else {
- // Fallback: Parse user input for tool calls (for models without native support)
- toolCalls := d.parseTextToolCalls(userInput)
- if len(toolCalls) > 0 {
- // Execute any tools found in user input
- for _, call := range toolCalls {
- result := d.executeTool(ctx, call)
-
- // Add tool result as context for next message
- d.chatHistory = append(d.chatHistory, api.Message{
- Role: "user",
- Content: fmt.Sprintf("Tool result for %s: %s", call.Name, result),
- })
- }
-
- // Get final response with tool results
- var finalResponse string
- err = d.client.Chat(ctx, &api.ChatRequest{
- Model: d.model,
- Messages: d.chatHistory,
- }, func(resp api.ChatResponse) error {
- finalResponse += resp.Message.Content
- return nil
- })
-
- if err == nil {
- d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: finalResponse})
- d.streamResponseChunks(ctx, finalResponse)
- }
- } else {
- // No tools, just stream the response
+ d.updateThinking("🧠 Processing with AI model...")
+
+ // No tools, get simple response (with timeout)
+ var fullResponse string
+ chatCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
+ defer cancel()
+
+ err := d.client.Chat(chatCtx, &api.ChatRequest{
+ Model: d.model,
+ Messages: d.chatHistory,
+ }, func(resp api.ChatResponse) error {
+ fullResponse += resp.Message.Content
+ return nil
+ })
+
+ d.stopThinking()
+ if err == nil {
+ d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: fullResponse})
d.streamResponseChunks(ctx, fullResponse)
+ } else {
+ d.emit(StreamMessage{
+ Type: MessageTypeSystem,
+ Error: err.Error(),
+ })
}
}
}
@@ -2092,16 +2166,42 @@ func (d *Del) renderUI() {
for msg := range d.output {
switch msg.Type {
case MessageTypeUser:
+ // Clear any existing line and just print the content (prompt already shown)
if currentLine != "" {
- fmt.Println()
+ fmt.Print("\r\033[K")
+ currentLine = ""
+ }
+ fmt.Printf("%s\n", msg.Content)
+
+ case "thinking":
+ switch msg.Status {
+ case "start", "update":
+ // Simple static thinking indicator
+ if currentLine != "" {
+ fmt.Print("\r\033[K")
+ }
+ line := fmt.Sprintf("🤖 Del: 🤔 %s", msg.Content)
+ fmt.Print(line)
+ currentLine = line
+
+ case "stop":
+ if currentLine != "" {
+ fmt.Print("\r\033[K")
+ // Show completion time if available
+ if msg.Content != "" {
+ fmt.Printf("⏱️ Completed in %s\n", msg.Content)
+ }
+ currentLine = ""
+ }
}
- fmt.Printf("🎤 You: %s\n", msg.Content)
- currentLine = ""
case MessageTypeAssistant:
+ // Clear any thinking indicator
if currentLine != "" {
- fmt.Println()
+ fmt.Print("\r\033[K")
+ currentLine = ""
}
+
fmt.Print("🤖 Del: ")
// Render markdown content
@@ -2116,11 +2216,11 @@ func (d *Del) renderUI() {
fmt.Printf(" %s\n", line) // 8 spaces to align with "🤖 Del: "
}
}
- currentLine = ""
case MessageTypeTool:
+ // Clear any thinking indicator
if currentLine != "" {
- fmt.Println()
+ fmt.Print("\r\033[K")
currentLine = ""
}
@@ -2141,10 +2241,14 @@ func (d *Del) renderUI() {
case "completed":
if msg.Result != "" {
lines := strings.Split(msg.Result, "\n")
+ timing := ""
+ if msg.Content != "" {
+ timing = fmt.Sprintf(" (⏱️ %s)", msg.Content)
+ }
if len(lines) <= 5 {
- fmt.Printf(" ⎿ %s\n", strings.ReplaceAll(msg.Result, "\n", "\n "))
+ fmt.Printf(" ⎿ %s%s\n", strings.ReplaceAll(msg.Result, "\n", "\n "), timing)
} else {
- fmt.Printf(" ⎿ %s (%d lines, ctrl+r to expand)\n", lines[0], len(lines))
+ fmt.Printf(" ⎿ %s (%d lines, ctrl+r to expand)%s\n", lines[0], len(lines), timing)
}
}
@@ -2193,6 +2297,7 @@ func (d *Del) Start(ctx context.Context) {
break
}
+ // Don't print the user input here since renderUI() will handle it
d.processMessage(ctx, input)
time.Sleep(100 * time.Millisecond) // Let final messages render
fmt.Println()