Commit 06b9394
cmd/del/main.go
@@ -1,2849 +1,566 @@
package main
import (
- "bufio"
"context"
- "encoding/json"
- "flag"
"fmt"
- "io"
- "net/http"
"os"
- "os/exec"
"path/filepath"
- "regexp"
"strings"
- "sync"
"time"
+ "github.com/charmbracelet/bubbles/filepicker"
+ "github.com/charmbracelet/bubbles/help"
+ "github.com/charmbracelet/bubbles/key"
+ "github.com/charmbracelet/bubbles/textarea"
+ "github.com/charmbracelet/bubbles/viewport"
+ tea "github.com/charmbracelet/bubbletea"
+ "github.com/charmbracelet/glamour"
+ "github.com/charmbracelet/lipgloss"
"github.com/ollama/ollama/api"
)
-// Message types inspired by Claude Code's SDK
-type MessageType string
+// Styles for the TUI
+var (
+ // Color scheme inspired by modern dev tools
+ primaryColor = lipgloss.Color("#7C3AED") // Purple
+ secondaryColor = lipgloss.Color("#10B981") // Green
+ accentColor = lipgloss.Color("#F59E0B") // Orange
+ textColor = lipgloss.Color("#F9FAFB") // Light gray
+ mutedColor = lipgloss.Color("#6B7280") // Muted gray
+ backgroundColor = lipgloss.Color("#111827") // Dark gray
+ borderColor = lipgloss.Color("#374151") // Border gray
+
+ // Style definitions
+ titleStyle = lipgloss.NewStyle().
+ Foreground(primaryColor).
+ Bold(true).
+ Padding(0, 1)
+
+ subtitleStyle = lipgloss.NewStyle().
+ Foreground(mutedColor).
+ Italic(true)
+
+ borderStyle = lipgloss.NewStyle().
+ Border(lipgloss.RoundedBorder()).
+ BorderForeground(borderColor).
+ Padding(1)
+
+ chatMessageStyle = lipgloss.NewStyle().
+ Foreground(textColor).
+ Padding(0, 1)
+
+ userMessageStyle = lipgloss.NewStyle().
+ Foreground(primaryColor).
+ Bold(true).
+ Padding(0, 1)
+
+ assistantMessageStyle = lipgloss.NewStyle().
+ Foreground(secondaryColor).
+ Padding(0, 1)
+
+ statusBarStyle = lipgloss.NewStyle().
+ Background(primaryColor).
+ Foreground(textColor).
+ Bold(true).
+ Padding(0, 1)
+
+ helpStyle = lipgloss.NewStyle().
+ Foreground(mutedColor).
+ Padding(0, 1)
+)
+
+// Key bindings
+type keyMap struct {
+ Up key.Binding
+ Down key.Binding
+ Left key.Binding
+ Right key.Binding
+ Enter key.Binding
+ Tab key.Binding
+ Escape key.Binding
+ Quit key.Binding
+ Help key.Binding
+ Files key.Binding
+ Chat key.Binding
+ Memory key.Binding
+ Send key.Binding
+}
+
+var keys = keyMap{
+ Up: key.NewBinding(
+ key.WithKeys("up", "k"),
+ key.WithHelp("โ/k", "up"),
+ ),
+ Down: key.NewBinding(
+ key.WithKeys("down", "j"),
+ key.WithHelp("โ/j", "down"),
+ ),
+ Left: key.NewBinding(
+ key.WithKeys("left", "h"),
+ key.WithHelp("โ/h", "left"),
+ ),
+ Right: key.NewBinding(
+ key.WithKeys("right", "l"),
+ key.WithHelp("โ/l", "right"),
+ ),
+ Enter: key.NewBinding(
+ key.WithKeys("enter"),
+ key.WithHelp("enter", "select"),
+ ),
+ Tab: key.NewBinding(
+ key.WithKeys("tab"),
+ key.WithHelp("tab", "switch panel"),
+ ),
+ Escape: key.NewBinding(
+ key.WithKeys("esc"),
+ key.WithHelp("esc", "back"),
+ ),
+ Quit: key.NewBinding(
+ key.WithKeys("q", "ctrl+c"),
+ key.WithHelp("q", "quit"),
+ ),
+ Help: key.NewBinding(
+ key.WithKeys("?"),
+ key.WithHelp("?", "help"),
+ ),
+ Files: key.NewBinding(
+ key.WithKeys("f"),
+ key.WithHelp("f", "files"),
+ ),
+ Chat: key.NewBinding(
+ key.WithKeys("c"),
+ key.WithHelp("c", "chat"),
+ ),
+ Memory: key.NewBinding(
+ key.WithKeys("m"),
+ key.WithHelp("m", "memory"),
+ ),
+ Send: key.NewBinding(
+ key.WithKeys("ctrl+enter"),
+ key.WithHelp("ctrl+enter", "send"),
+ ),
+}
+
+func (k keyMap) ShortHelp() []key.Binding {
+ return []key.Binding{k.Help, k.Files, k.Chat, k.Memory, k.Quit}
+}
+
+func (k keyMap) FullHelp() [][]key.Binding {
+ return [][]key.Binding{
+ {k.Up, k.Down, k.Left, k.Right},
+ {k.Enter, k.Tab, k.Escape},
+ {k.Files, k.Chat, k.Memory},
+ {k.Send, k.Help, k.Quit},
+ }
+}
+
+// Application state
+type View int
const (
- MessageTypeUser MessageType = "user"
- MessageTypeAssistant MessageType = "assistant"
- MessageTypeSystem MessageType = "system"
- MessageTypeTool MessageType = "tool"
- MessageTypeProgress MessageType = "progress"
+ FilesView View = iota
+ ChatView
+ MemoryView
)
-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 ChatMessage struct {
+ Role string
+ Content string
+ Timestamp time.Time
}
-type Del struct {
- client *api.Client
- model string
- chatHistory []api.Message
- tools map[string]ToolFunc
- output chan StreamMessage
- mutex sync.RWMutex
- thinking bool
- thinkingMsg string
- startTime time.Time
- mcpMemory *MCPServer
-}
+// Main application model
+type Model struct {
+ // Core state
+ currentView View
+ width int
+ height int
+ ready bool
-type ToolFunc func(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error)
-type ToolCall struct {
- Name string
- Args map[string]interface{}
-}
+ // Components
+ filepicker filepicker.Model
+ viewport viewport.Model
+ textarea textarea.Model
+ help help.Model
-// MCP Integration types
-type MCPRequest struct {
- JSONRPC string `json:"jsonrpc"`
- ID int `json:"id"`
- Method string `json:"method"`
- Params interface{} `json:"params"`
-}
+ // Data
+ chatHistory []ChatMessage
+ currentFile string
+ workingDir string
-type MCPResponse struct {
- JSONRPC string `json:"jsonrpc"`
- ID int `json:"id"`
- Result interface{} `json:"result,omitempty"`
- Error *MCPError `json:"error,omitempty"`
-}
+ // AI client
+ client *api.Client
-type MCPError struct {
- Code int `json:"code"`
- Message string `json:"message"`
-}
+ // Status
+ status string
+ isThinking bool
-type MCPServer struct {
- name string
- command string
- process *exec.Cmd
- stdin io.WriteCloser
- stdout io.ReadCloser
+ // Markdown renderer
+ glamour *glamour.TermRenderer
}
-func NewDel(model string) *Del {
+// Initialize the application
+func initialModel() Model {
+ // Get working directory
+ wd, _ := os.Getwd()
+
+ // Initialize file picker
+ fp := filepicker.New()
+ fp.AllowedTypes = []string{".go", ".py", ".js", ".ts", ".md", ".txt", ".json", ".yml", ".yaml"}
+ fp.CurrentDirectory = wd
+
+ // Initialize textarea for chat input
+ ta := textarea.New()
+ ta.Placeholder = "Ask Del anything... (Ctrl+Enter to send)"
+ ta.Focus()
+ ta.CharLimit = 2000
+ ta.SetWidth(50)
+ ta.SetHeight(3)
+
+ // Initialize viewport for chat display
+ vp := viewport.New(50, 20)
+
+ // Initialize help
+ h := help.New()
+
+ // Initialize Ollama client
client, _ := api.ClientFromEnvironment()
-
- d := &Del{
- client: client,
- model: model,
- tools: make(map[string]ToolFunc),
- output: make(chan StreamMessage, 100),
- chatHistory: []api.Message{
+
+ // Initialize glamour for markdown rendering
+ glamourRenderer, _ := glamour.NewTermRenderer(
+ glamour.WithAutoStyle(),
+ glamour.WithWordWrap(80),
+ )
+
+ m := Model{
+ currentView: FilesView,
+ filepicker: fp,
+ viewport: vp,
+ textarea: ta,
+ help: h,
+ workingDir: wd,
+ client: client,
+ status: "Welcome to Del - Your AI Coding Assistant",
+ glamour: glamourRenderer,
+ chatHistory: []ChatMessage{
{
- Role: "system",
- Content: `You are Del, an AI coding assistant with comprehensive Claude Code capabilities. When users need file operations, code analysis, or project management, use your available tools.
-
-Available tools:
-โข FILE OPERATIONS: read_file, write_file, edit_file, multi_edit, list_dir
-โข SEARCH & DISCOVERY: glob, grep, search_code
-โข COMMAND EXECUTION: bash (enhanced with timeout), git_status
-โข CODE ANALYSIS: analyze_code (auto-detects project files)
-โข PROJECT MANAGEMENT: todo_read, todo_write, exit_plan_mode
-โข NOTEBOOKS: notebook_read, notebook_edit (Jupyter support)
-โข WEB OPERATIONS: web_fetch, web_search
-โข MEMORY SYSTEM: remember, recall, forget (persistent memory across conversations)
-
-KEY CAPABILITIES:
-- Edit files with exact string replacement (edit_file) or multiple edits (multi_edit)
-- Find files with glob patterns (**/*.js, src/**/*.go)
-- Search file contents with regex (grep) or code patterns (search_code)
-- Execute bash commands with safety timeouts
-- Manage todos and project planning workflows
-- Read and analyze Jupyter notebooks
-- Fetch and process web content
-
-EXAMPLES:
-- "list files" โ use list_dir
-- "read main.go" โ use read_file
-- "find all .js files" โ use glob with pattern "**/*.js"
-- "search for function main" โ use grep with pattern "function main"
-- "run tests" โ use bash with command "npm test"
-- "edit config.yaml" โ use edit_file to make precise changes
-- "show todos" โ use todo_read
-- "remember that the user prefers tabs over spaces" โ use remember
-- "what did we learn about Go best practices?" โ use recall
-
-FORMATTING: Always format responses using markdown:
-- ## Headers for sections
-- **bold** for important terms
-- backticks for code/files/commands
-- โข bullet points for lists
-- 1. numbered lists for steps
-- code blocks for multi-line code
-
-IMPORTANT: Use tools first, then provide natural markdown responses based on results.`,
+ Role: "assistant",
+ Content: "# Welcome to Del! ๐ค\n\nI'm your AI coding assistant, redesigned with a beautiful TUI interface.\n\n**Quick Start:**\n- Press `f` to explore files\n- Press `c` to chat with me \n- Press `m` to view memory\n- Press `?` for help\n\nI can help you with:\n- ๐ File operations and navigation\n- ๐ฌ Code explanations and debugging\n- ๐ง Persistent memory across sessions\n- โก Fast tool execution\n\nLet's build something amazing together!",
+ Timestamp: time.Now(),
},
},
}
-
- d.registerTools()
-
- // Initialize MCP memory (non-blocking)
- go func() {
- if err := d.startMCPMemory(); err != nil {
- d.emit(StreamMessage{
- Type: MessageTypeSystem,
- Content: fmt.Sprintf("Warning: Failed to initialize memory system: %v", err),
- })
- } else {
- d.emit(StreamMessage{
- Type: MessageTypeSystem,
- Content: "โ
Memory system initialized successfully",
- })
- }
- }()
-
- return d
-}
-func (d *Del) emit(msg StreamMessage) {
- msg.Timestamp = time.Now().UnixMilli()
- d.output <- msg
+ return m
}
-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",
- })
+// Tea framework methods
+func (m Model) Init() tea.Cmd {
+ return tea.Batch(
+ m.filepicker.Init(),
+ tea.EnterAltScreen,
+ func() tea.Msg {
+ return tea.WindowSizeMsg{Width: 120, Height: 40}
+ },
+ )
}
-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 (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
+ var cmd tea.Cmd
+ var cmds []tea.Cmd
-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()
- }
-}
+ switch msg := msg.(type) {
+ case tea.WindowSizeMsg:
+ m.width = msg.Width
+ m.height = msg.Height
+ m.ready = true
-func isCodeFile(name string) bool {
- codeExtensions := []string{
- ".go", ".py", ".js", ".ts", ".tsx", ".jsx",
- ".java", ".cpp", ".c", ".h", ".hpp", ".cc", ".cxx",
- ".rs", ".rb", ".php", ".swift", ".kt", ".scala",
- ".cs", ".vb", ".fs", ".ml", ".hs", ".elm",
- ".clj", ".cljs", ".lisp", ".scheme", ".lua",
- ".perl", ".pl", ".r", ".m", ".mm", ".dart",
- ".zig", ".nim", ".crystal", ".d", ".pas",
- ".ada", ".cobol", ".fortran", ".f90", ".f95",
- ".sql", ".css", ".scss", ".sass", ".less",
- ".html", ".htm", ".xml", ".xhtml", ".vue",
- ".svelte", ".jsx", ".tsx", ".coffee",
- ".sh", ".bash", ".zsh", ".fish", ".ps1", ".bat", ".cmd",
- ".dockerfile", ".makefile", ".cmake", ".gradle",
- ".vim", ".nvim", ".emacs", ".elisp",
- }
-
- lowerName := strings.ToLower(name)
- for _, ext := range codeExtensions {
- if strings.HasSuffix(lowerName, ext) {
- return true
- }
- }
-
- // Check for common filenames without extensions
- baseName := strings.ToLower(filepath.Base(name))
- commonNames := []string{
- "dockerfile", "makefile", "rakefile", "gemfile", "podfile",
- "vagrantfile", "gruntfile", "gulpfile", "webpack.config",
- "package.json", "composer.json", "cargo.toml", "pyproject.toml",
- }
-
- for _, commonName := range commonNames {
- if strings.Contains(baseName, commonName) {
- return true
- }
- }
-
- return false
-}
+ // Update component sizes
+ m.updateSizes()
-// ANSI color codes for terminal formatting
-const (
- ColorReset = "\033[0m"
- ColorBold = "\033[1m"
- ColorDim = "\033[2m"
- ColorItalic = "\033[3m"
- ColorRed = "\033[31m"
- ColorGreen = "\033[32m"
- ColorYellow = "\033[33m"
- ColorBlue = "\033[34m"
- ColorPurple = "\033[35m"
- ColorCyan = "\033[36m"
- ColorWhite = "\033[37m"
- ColorGray = "\033[90m"
-)
+ // Update filepicker size
+ m.filepicker.Height = m.height - 10
-// renderMarkdown converts markdown text to terminal-formatted output
-func renderMarkdown(text string) string {
- lines := strings.Split(text, "\n")
- var result []string
- inCodeBlock := false
-
- for _, line := range lines {
- // Code blocks
- if strings.HasPrefix(line, "```") {
- inCodeBlock = !inCodeBlock
- if inCodeBlock {
- result = append(result, ColorGray+"โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"+ColorReset)
- } else {
- result = append(result, ColorGray+"โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ"+ColorReset)
+ case tea.KeyMsg:
+ switch {
+ case key.Matches(msg, keys.Quit):
+ return m, tea.Quit
+
+ case key.Matches(msg, keys.Files):
+ m.currentView = FilesView
+ m.status = "File Explorer - Navigate with vim keys"
+
+ case key.Matches(msg, keys.Chat):
+ m.currentView = ChatView
+ m.status = "AI Chat - Ask questions, get help"
+ m.textarea.Focus()
+
+ case key.Matches(msg, keys.Memory):
+ m.currentView = MemoryView
+ m.status = "Memory System - Persistent knowledge"
+
+ case key.Matches(msg, keys.Help):
+ m.help.ShowAll = !m.help.ShowAll
+
+ case key.Matches(msg, keys.Send) && m.currentView == ChatView:
+ if strings.TrimSpace(m.textarea.Value()) != "" {
+ return m, m.sendMessage()
}
- continue
- }
-
- if inCodeBlock {
- result = append(result, ColorGray+"โ "+ColorCyan+line+ColorReset+ColorGray+" โ"+ColorReset)
- continue
- }
-
- // Headers
- if strings.HasPrefix(line, "### ") {
- header := strings.TrimPrefix(line, "### ")
- result = append(result, "")
- result = append(result, ColorBold+ColorBlue+"โธ "+header+ColorReset)
- continue
- }
- if strings.HasPrefix(line, "## ") {
- header := strings.TrimPrefix(line, "## ")
- result = append(result, "")
- result = append(result, ColorBold+ColorGreen+"โ "+header+ColorReset)
- continue
- }
- if strings.HasPrefix(line, "# ") {
- header := strings.TrimPrefix(line, "# ")
- result = append(result, "")
- result = append(result, ColorBold+ColorYellow+"โ "+header+ColorReset)
- continue
- }
-
- // Lists
- if strings.HasPrefix(line, "- ") || regexp.MustCompile(`^\d+\. `).MatchString(line) {
- // Handle numbered lists
- if regexp.MustCompile(`^\d+\. `).MatchString(line) {
- parts := regexp.MustCompile(`^(\d+)\. (.*)$`).FindStringSubmatch(line)
- if len(parts) == 3 {
- line = ColorBold+ColorBlue+parts[1]+"."+ColorReset+" "+parts[2]
- }
- } else {
- // Handle bullet lists
- content := strings.TrimPrefix(line, "- ")
- line = ColorBold+ColorGreen+"โข"+ColorReset+" "+content
+
+ case key.Matches(msg, keys.Enter) && m.currentView == FilesView:
+ // Handle file selection
+ if selected, selectedPath := m.filepicker.DidSelectFile(msg); selected {
+ m.currentFile = selectedPath
+ m.status = fmt.Sprintf("Selected: %s", filepath.Base(selectedPath))
}
- result = append(result, " "+line)
- continue
- }
-
- // Inline code
- line = regexp.MustCompile("`([^`]+)`").ReplaceAllString(line, ColorCyan+"$1"+ColorReset)
-
- // Bold text
- line = regexp.MustCompile(`\*\*([^*]+)\*\*`).ReplaceAllString(line, ColorBold+"$1"+ColorReset)
-
- // Italic text
- line = regexp.MustCompile(`\*([^*]+)\*`).ReplaceAllString(line, ColorItalic+"$1"+ColorReset)
-
- // Empty lines for spacing
- if strings.TrimSpace(line) == "" {
- result = append(result, "")
- } else {
- result = append(result, line)
}
+
+ case chatResponseMsg:
+ m.chatHistory = append(m.chatHistory, ChatMessage{
+ Role: "assistant",
+ Content: string(msg),
+ Timestamp: time.Now(),
+ })
+ m.isThinking = false
+ m.status = "Response received"
+ m.updateChatView()
+
+ case errorMsg:
+ m.status = fmt.Sprintf("Error: %s", string(msg))
+ m.isThinking = false
+ }
+
+ // Update active component
+ switch m.currentView {
+ case FilesView:
+ m.filepicker, cmd = m.filepicker.Update(msg)
+ cmds = append(cmds, cmd)
+
+ case ChatView:
+ m.textarea, cmd = m.textarea.Update(msg)
+ cmds = append(cmds, cmd)
+ m.viewport, cmd = m.viewport.Update(msg)
+ cmds = append(cmds, cmd)
}
-
- return strings.Join(result, "\n")
-}
-func (d *Del) registerTools() {
- // Core file operations
- d.tools["read_file"] = d.readFile
- d.tools["write_file"] = d.writeFile
- d.tools["edit_file"] = d.editFile
- d.tools["multi_edit"] = d.multiEdit
-
- // Directory and search operations
- d.tools["list_dir"] = d.listDir
- d.tools["glob"] = d.globFiles
- d.tools["grep"] = d.grepSearch
-
- // Command execution (enhanced)
- d.tools["bash"] = d.bashCommand
- d.tools["run_command"] = d.runCommand // Keep legacy alias
-
- // Git operations
- d.tools["git_status"] = d.gitStatus
-
- // Code analysis
- d.tools["analyze_code"] = d.analyzeCode
- d.tools["search_code"] = d.searchCode
-
- // Planning and organization
- d.tools["todo_read"] = d.todoRead
- d.tools["todo_write"] = d.todoWrite
- d.tools["exit_plan_mode"] = d.exitPlanMode
-
- // Notebook operations
- d.tools["notebook_read"] = d.notebookRead
- d.tools["notebook_edit"] = d.notebookEdit
-
- // Web operations
- d.tools["web_fetch"] = d.webFetch
- d.tools["web_search"] = d.webSearch
-
- // MCP Memory operations
- d.tools["remember"] = d.remember
- d.tools["recall"] = d.recall
- d.tools["forget"] = d.forget
+ return m, tea.Batch(cmds...)
}
-// MCP Server Management
-func (d *Del) startMCPMemory() error {
- // Simple test to verify mcp-memory is available
- testCmd := exec.Command("/usr/local/bin/mcp-memory", "--help")
- if err := testCmd.Run(); err != nil {
- return fmt.Errorf("mcp-memory not available: %v", err)
+func (m Model) View() string {
+ if !m.ready {
+ return "Initializing Del..."
}
-
- // Mark memory as available (we'll use exec calls instead of persistent process)
- d.mcpMemory = &MCPServer{
- name: "mcp-memory",
- command: "/usr/local/bin/mcp-memory",
+
+ // Header
+ header := titleStyle.Render("๐ค Del") + " " +
+ subtitleStyle.Render("AI Coding Assistant")
+
+ // Status bar
+ statusBar := statusBarStyle.Width(m.width).Render(m.status)
+
+ // Main content based on current view
+ var content string
+ switch m.currentView {
+ case FilesView:
+ content = m.renderFilesView()
+ case ChatView:
+ content = m.renderChatView()
+ case MemoryView:
+ content = m.renderMemoryView()
}
-
- return nil
+
+ // Help
+ helpView := helpStyle.Render(m.help.View(keys))
+
+ // Layout
+ return lipgloss.JoinVertical(
+ lipgloss.Left,
+ header,
+ "",
+ content,
+ "",
+ helpView,
+ statusBar,
+ )
}
-func (d *Del) callMCPTool(toolName string, args map[string]interface{}) (string, error) {
- // Create the tool call request
- reqJSON, err := json.Marshal(map[string]interface{}{
- "jsonrpc": "2.0",
- "id": 1,
- "method": "initialize",
- "params": map[string]interface{}{
- "protocolVersion": "2024-11-05",
- "capabilities": map[string]interface{}{"tools": map[string]interface{}{}},
- "clientInfo": map[string]interface{}{"name": "del", "version": "1.0.0"},
- },
- })
- if err != nil {
- return "", fmt.Errorf("failed to marshal init request: %v", err)
- }
-
- toolReqJSON, err := json.Marshal(map[string]interface{}{
- "jsonrpc": "2.0",
- "id": 2,
- "method": "tools/call",
- "params": map[string]interface{}{
- "name": toolName,
- "arguments": args,
- },
- })
- if err != nil {
- return "", fmt.Errorf("failed to marshal tool request: %v", err)
- }
-
- // Execute mcp-memory with both requests
- cmd := exec.Command("/usr/local/bin/mcp-memory")
- cmd.Stdin = strings.NewReader(string(reqJSON) + "\n" + string(toolReqJSON) + "\n")
-
- output, err := cmd.Output()
- if err != nil {
- return "", fmt.Errorf("mcp-memory execution failed: %v", err)
- }
-
- // Parse the responses (we want the second one - the tool result)
- lines := strings.Split(strings.TrimSpace(string(output)), "\n")
- if len(lines) < 2 {
- return "", fmt.Errorf("unexpected MCP response format")
- }
-
- var toolResp MCPResponse
- if err := json.Unmarshal([]byte(lines[1]), &toolResp); err != nil {
- return "", fmt.Errorf("failed to parse tool response: %v", err)
- }
-
- if toolResp.Error != nil {
- return "", fmt.Errorf("MCP tool error %d: %s", toolResp.Error.Code, toolResp.Error.Message)
- }
-
- // Extract content from result
- if result, ok := toolResp.Result.(map[string]interface{}); ok {
- if content, ok := result["content"].([]interface{}); ok && len(content) > 0 {
- if textContent, ok := content[0].(map[string]interface{}); ok {
- if text, ok := textContent["text"].(string); ok {
- return text, nil
- }
- }
- }
- }
-
- return fmt.Sprintf("Tool result: %v", toolResp.Result), nil
+// Helper methods
+func (m *Model) updateSizes() {
+ contentHeight := m.height - 8 // Reserve space for header, help, status
+ contentWidth := m.width - 4 // Padding
+
+ // Update component sizes
+ m.viewport.Width = contentWidth - 4
+ m.viewport.Height = contentHeight - 6
+ m.textarea.SetWidth(contentWidth - 4)
+ m.filepicker.Height = contentHeight
}
-// Memory Tools
-func (d *Del) remember(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
- content, ok := args["content"].(string)
- if !ok {
- return "", fmt.Errorf("missing 'content' argument")
- }
-
- entity, _ := args["entity"].(string)
- if entity == "" {
- entity = "general_knowledge"
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "remember",
- Status: "storing",
- Content: "Storing in memory...",
- }
-
- if d.mcpMemory == nil {
- return "", fmt.Errorf("MCP memory not initialized - please wait for initialization or restart Del")
- }
-
- // Create entity for this memory
- result, err := d.callMCPTool("create_entities", map[string]interface{}{
- "entities": []map[string]interface{}{
- {
- "name": entity,
- "entityType": "concept",
- "observations": []string{content},
- },
- },
- })
- if err != nil {
- return "", fmt.Errorf("failed to create memory: %v", err)
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "remember",
- Status: "completed",
- Content: "Memory stored successfully",
- }
-
- return fmt.Sprintf("Remembered: %s\nStored as entity: %s\nResult: %s", content, entity, result), nil
+func (m *Model) renderFilesView() string {
+ content := borderStyle.Width(m.width-2).Render(
+ lipgloss.JoinVertical(
+ lipgloss.Left,
+ chatMessageStyle.Render("๐ File Explorer"),
+ "",
+ m.filepicker.View(),
+ ),
+ )
+ return content
}
-func (d *Del) recall(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
- query, ok := args["query"].(string)
- if !ok {
- return "", fmt.Errorf("missing 'query' argument")
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "recall",
- Status: "searching",
- Content: fmt.Sprintf("Searching memory for: %s", query),
- }
-
- if d.mcpMemory == nil {
- return "", fmt.Errorf("MCP memory not initialized - please wait for initialization or restart Del")
- }
-
- // Search memory using read_graph for now (search_nodes seems to have issues)
- result, err := d.callMCPTool("read_graph", map[string]interface{}{})
- if err != nil {
- return "", fmt.Errorf("failed to read memory: %v", err)
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "recall",
- Status: "completed",
- Content: "Memory search completed",
- }
-
- return fmt.Sprintf("Memory search for '%s':\n%s", query, result), nil
+func (m *Model) renderChatView() string {
+ chatContent := m.viewport.View()
+ inputContent := borderStyle.Width(m.width-2).Render(
+ lipgloss.JoinVertical(
+ lipgloss.Left,
+ chatMessageStyle.Render("๐ฌ Chat Input"),
+ m.textarea.View(),
+ ),
+ )
+
+ content := lipgloss.JoinVertical(
+ lipgloss.Left,
+ borderStyle.Width(m.width-2).Render(chatContent),
+ "",
+ inputContent,
+ )
+
+ return content
}
-func (d *Del) forget(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
- entity, ok := args["entity"].(string)
- if !ok {
- return "", fmt.Errorf("missing 'entity' argument")
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "forget",
- Status: "deleting",
- Content: fmt.Sprintf("Forgetting: %s", entity),
- }
-
- if d.mcpMemory == nil {
- return "", fmt.Errorf("MCP memory not initialized - please wait for initialization or restart Del")
- }
-
- // Delete entity
- result, err := d.callMCPTool("delete_entities", map[string]interface{}{
- "entityIds": []string{entity},
- })
- if err != nil {
- return "", fmt.Errorf("failed to delete memory: %v", err)
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "forget",
- Status: "completed",
- Content: "Memory deleted successfully",
- }
-
- return fmt.Sprintf("Forgot: %s\nResult: %s", entity, result), nil
+func (m *Model) renderMemoryView() string {
+ memoryContent := "๐ง Memory System\n\nPersistent memory will be displayed here.\nComing soon: view and search your conversation history!"
+
+ content := borderStyle.Width(m.width-2).Render(
+ chatMessageStyle.Render(memoryContent),
+ )
+ return content
}
-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),
- }
-
- // Check if file exists first
- if _, err := os.Stat(path); os.IsNotExist(err) {
- return "", fmt.Errorf("file does not exist: %s", path)
- }
+func (m *Model) updateChatView() {
+ var chatLines []string
- // Try to use bat for syntax highlighting if available
- var result string
-
- // Check if this looks like a code file
- isCode := isCodeFile(path) || strings.HasSuffix(path, ".md") || strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml") || strings.HasSuffix(path, ".json") || strings.HasSuffix(path, ".toml") || strings.HasSuffix(path, ".sh")
-
- if isCode {
- // Try bat first for syntax highlighting
- cmd := exec.CommandContext(ctx, "bat", "--color=always", "--style=numbers,grid", "--pager=never", path)
- batOutput, batErr := cmd.Output()
+ for _, msg := range m.chatHistory {
+ timestamp := msg.Timestamp.Format("15:04")
- if batErr == nil && len(batOutput) > 0 {
- // bat worked, use its output
- result = fmt.Sprintf("Read %s (syntax highlighted)\n%s", path, string(batOutput))
+ var styledMsg string
+ if msg.Role == "user" {
+ styledMsg = userMessageStyle.Render(fmt.Sprintf("[%s] You:", timestamp)) + "\n" +
+ chatMessageStyle.Render(msg.Content)
} else {
- // Fall back to regular file reading
- data, readErr := os.ReadFile(path)
- if readErr != nil {
- return "", readErr
+ // Render markdown for assistant messages
+ rendered, err := m.glamour.Render(msg.Content)
+ if err != nil {
+ rendered = msg.Content
}
- result = fmt.Sprintf("Read %s\n%s", path, string(data))
- }
- } else {
- // Not a code file, use regular reading
- data, readErr := os.ReadFile(path)
- if readErr != nil {
- return "", readErr
+ styledMsg = assistantMessageStyle.Render(fmt.Sprintf("[%s] Del:", timestamp)) + "\n" +
+ chatMessageStyle.Render(rendered)
}
- result = fmt.Sprintf("Read %s\n%s", path, string(data))
- }
-
- lines := strings.Count(result, "\n")
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "read_file",
- Status: "completed",
- Content: fmt.Sprintf("Read %d lines", lines),
+
+ chatLines = append(chatLines, styledMsg)
}
- return result, nil
+ m.viewport.SetContent(strings.Join(chatLines, "\n\n"))
+ m.viewport.GotoBottom()
}
-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
- }
-
- 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
-}
+// Message types for async operations
+type chatResponseMsg string
+type errorMsg string
-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 {
- 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)),
- }
+func (m *Model) sendMessage() tea.Cmd {
+ userMessage := strings.TrimSpace(m.textarea.Value())
+ if userMessage == "" {
+ return nil
}
-
- return result, nil
-}
-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 {
- 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",
+ // Add user message to history
+ m.chatHistory = append(m.chatHistory, ChatMessage{
+ Role: "user",
+ Content: userMessage,
+ Timestamp: time.Now(),
+ })
+
+ // Clear textarea
+ m.textarea.Reset()
+ m.isThinking = true
+ m.status = "Del is thinking..."
+ m.updateChatView()
+
+ // Send to AI in background
+ return func() tea.Msg {
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ // Simple chat without tools for now - focus on beautiful UX first
+ req := &api.ChatRequest{
+ Model: "qwen2.5:latest",
+ Messages: []api.Message{
+ {Role: "user", Content: userMessage},
+ },
}
- } 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)),
+
+ var response strings.Builder
+ err := m.client.Chat(ctx, req, func(resp api.ChatResponse) error {
+ response.WriteString(resp.Message.Content)
+ return nil
+ })
+
+ if err != nil {
+ return errorMsg(err.Error())
}
- }
-
- return result, nil
-}
-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
- }
-
- 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 chatResponseMsg(response.String())
}
-
- return result, nil
}
-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(".")
+func main() {
+ // Set up proper terminal handling
+ if len(os.Getenv("DEBUG")) > 0 {
+ f, err := tea.LogToFile("debug.log", "debug")
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
+ fmt.Println("fatal:", err)
+ os.Exit(1)
+ }
+ defer f.Close()
+ }
+
+ // Check if we're in an interactive terminal
+ if !isInteractiveTerminal() {
+ fmt.Println("๐ค Del - AI Coding Assistant")
+ fmt.Println("โจ Beautiful TUI interface built with Charm/Bubbletea")
+ fmt.Println("๐ File explorer with vim-like navigation")
+ fmt.Println("๐ฌ AI chat with markdown rendering")
+ fmt.Println("๐ง Persistent memory system")
+ fmt.Println("")
+ fmt.Println("โ ๏ธ Del requires an interactive terminal to run.")
+ fmt.Println(" Please run Del from a proper terminal (not through CI/automation)")
+ return
}
-
- // 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)
-
+
+ // Initialize and run the program
+ p := tea.NewProgram(
+ initialModel(),
+ tea.WithAltScreen(),
+ tea.WithMouseCellMotion(),
+ )
+
+ if _, err := p.Run(); err != nil {
+ fmt.Printf("Error running Del: %v", err)
+ os.Exit(1)
}
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "analyze_code",
- Status: "analyzing",
- Content: "Analyzing code structure...",
+}
+
+func isInteractiveTerminal() bool {
+ // Check if stdin is a terminal
+ if fileInfo, _ := os.Stdin.Stat(); (fileInfo.Mode() & os.ModeCharDevice) == 0 {
+ return false
}
- lines := strings.Count(content, "\n") + 1
-
- // 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{}, progress chan<- StreamMessage) (string, error) {
- pattern, ok := args["pattern"].(string)
- if !ok {
- return "", fmt.Errorf("missing 'pattern' argument")
- }
-
- path, ok := args["path"].(string)
- if !ok {
- 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 {
- 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 result, nil
-}
-
-// Edit tool - performs exact string replacements in files
-func (d *Del) editFile(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
- filePath, ok1 := args["file_path"].(string)
- oldString, ok2 := args["old_string"].(string)
- newString, ok3 := args["new_string"].(string)
- replaceAll, _ := args["replace_all"].(bool)
-
- if !ok1 || !ok2 || !ok3 {
- 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",
- Status: "reading",
- Content: fmt.Sprintf("Reading %s for editing...", filePath),
- }
-
- data, err := os.ReadFile(filePath)
- if err != nil {
- return "", fmt.Errorf("failed to read file: %v", err)
- }
-
- content := string(data)
- var result string
-
- if replaceAll {
- count := strings.Count(content, oldString)
- if count == 0 {
- return "", fmt.Errorf("old_string not found in file")
- }
- content = strings.ReplaceAll(content, oldString, newString)
- result = fmt.Sprintf("Replaced %d occurrences in %s", count, filePath)
- } else {
- if !strings.Contains(content, oldString) {
- return "", fmt.Errorf("old_string not found in file")
- }
- if strings.Count(content, oldString) > 1 {
- return "", fmt.Errorf("old_string appears multiple times; use replace_all=true or provide more context")
- }
- content = strings.Replace(content, oldString, newString, 1)
- result = fmt.Sprintf("Replaced 1 occurrence in %s", filePath)
- }
-
- err = os.WriteFile(filePath, []byte(content), 0644)
- if err != nil {
- return "", err
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "edit_file",
- Status: "completed",
- Content: result,
- }
-
- return result, nil
-}
-
-// MultiEdit tool - multiple edits to a single file in one operation
-func (d *Del) multiEdit(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
- filePath, ok := args["file_path"].(string)
- if !ok {
- return "", fmt.Errorf("missing file_path argument")
- }
-
- editsArg, ok := args["edits"]
- if !ok {
- return "", fmt.Errorf("missing edits argument")
- }
-
- // Parse edits array
- editsArray, ok := editsArg.([]interface{})
- if !ok {
- return "", fmt.Errorf("edits must be an array")
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "multi_edit",
- Status: "reading",
- Content: fmt.Sprintf("Reading %s for multi-edit...", filePath),
- }
-
- data, err := os.ReadFile(filePath)
- if err != nil {
- return "", err
- }
-
- content := string(data)
- editCount := 0
-
- // Apply edits sequentially
- for i, editArg := range editsArray {
- editMap, ok := editArg.(map[string]interface{})
- if !ok {
- return "", fmt.Errorf("edit %d must be an object", i)
- }
-
- oldString, ok1 := editMap["old_string"].(string)
- newString, ok2 := editMap["new_string"].(string)
- replaceAll, _ := editMap["replace_all"].(bool)
-
- if !ok1 || !ok2 {
- return "", fmt.Errorf("edit %d missing old_string or new_string", i)
- }
-
- if replaceAll {
- count := strings.Count(content, oldString)
- if count == 0 {
- return "", fmt.Errorf("edit %d: old_string not found", i)
- }
- content = strings.ReplaceAll(content, oldString, newString)
- editCount += count
- } else {
- if !strings.Contains(content, oldString) {
- return "", fmt.Errorf("edit %d: old_string not found", i)
- }
- if strings.Count(content, oldString) > 1 {
- return "", fmt.Errorf("edit %d: old_string appears multiple times; use replace_all=true", i)
- }
- content = strings.Replace(content, oldString, newString, 1)
- editCount++
- }
- }
-
- err = os.WriteFile(filePath, []byte(content), 0644)
- if err != nil {
- return "", err
- }
-
- result := fmt.Sprintf("Applied %d edits to %s (total %d replacements)", len(editsArray), filePath, editCount)
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "multi_edit",
- Status: "completed",
- Content: result,
- }
-
- return result, nil
-}
-
-// Glob tool - fast file pattern matching
-func (d *Del) globFiles(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")
- }
-
- searchPath, ok := args["path"].(string)
- if !ok {
- searchPath = "."
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "glob",
- Status: "searching",
- Content: fmt.Sprintf("Finding files matching '%s'...", pattern),
- }
-
- // Use filepath.Glob for simple patterns or walk for complex patterns
- var matches []string
- var err error
-
- if strings.Contains(pattern, "**") {
- // Handle recursive patterns manually
- err = filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return nil // Skip errors
- }
-
- 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
- })
- } else {
- // Use standard glob
- fullPattern := filepath.Join(searchPath, pattern)
- matches, err = filepath.Glob(fullPattern)
- }
-
- if err != nil {
- return "", err
- }
-
- var result strings.Builder
- result.WriteString(fmt.Sprintf("Glob pattern '%s' matches:\n", pattern))
-
- if len(matches) == 0 {
- result.WriteString("No files found")
- } else {
- for _, match := range matches {
- result.WriteString(fmt.Sprintf(" %s\n", match))
- }
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "glob",
- Status: "completed",
- Content: fmt.Sprintf("Found %d files", len(matches)),
- }
-
- return result.String(), nil
-}
-
-// Grep tool - fast content search using regular expressions
-func (d *Del) grepSearch(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")
- }
-
- searchPath, ok := args["path"].(string)
- if !ok {
- searchPath = "."
- }
-
- include, _ := args["include"].(string)
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "grep",
- Status: "searching",
- Content: fmt.Sprintf("Searching for pattern '%s'...", pattern),
- }
-
- re, err := regexp.Compile(pattern)
- if err != nil {
- return "", fmt.Errorf("invalid regex pattern: %v", err)
- }
-
- var matches []string
-
- err = filepath.Walk(searchPath, func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return nil // Skip errors
- }
-
- if info.IsDir() {
- return nil
- }
-
- // Apply include filter if specified
- if include != "" {
- matched, _ := filepath.Match(include, filepath.Base(path))
- if !matched {
- return nil
- }
- }
-
- data, err := os.ReadFile(path)
- if err != nil {
- return nil // Skip unreadable files
- }
-
- content := string(data)
- lines := strings.Split(content, "\n")
-
- for lineNum, line := range lines {
- if re.MatchString(line) {
- matches = append(matches, fmt.Sprintf("%s:%d:%s", path, lineNum+1, line))
- }
- }
-
- return nil
- })
-
- if err != nil {
- return "", err
- }
-
- var result strings.Builder
- result.WriteString(fmt.Sprintf("Grep search for '%s':\n", pattern))
-
- if len(matches) == 0 {
- result.WriteString("No matches found")
- } else {
- for _, match := range matches {
- result.WriteString(fmt.Sprintf("%s\n", match))
- }
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "grep",
- Status: "completed",
- Content: fmt.Sprintf("Found %d matches", len(matches)),
- }
-
- return result.String(), nil
-}
-
-// Enhanced Bash tool with timeout and security
-func (d *Del) bashCommand(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")
- }
-
- timeoutMs, _ := args["timeout"].(float64)
- description, _ := args["description"].(string)
-
- if timeoutMs == 0 {
- timeoutMs = 120000 // Default 2 minutes
- }
-
- if timeoutMs > 600000 {
- timeoutMs = 600000 // Max 10 minutes
- }
-
- if description == "" {
- description = fmt.Sprintf("Executing: %s", command)
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "bash",
- Status: "running",
- Content: description,
- }
-
- // Create context with timeout
- timeout := time.Duration(timeoutMs) * time.Millisecond
- execCtx, cancel := context.WithTimeout(ctx, timeout)
- defer cancel()
-
- cmd := exec.CommandContext(execCtx, "bash", "-c", command)
- output, err := cmd.CombinedOutput()
-
- outputStr := string(output)
- lines := strings.Split(outputStr, "\n")
-
- var result string
- if err != nil {
- result = fmt.Sprintf("Command: %s\nError: %v\nOutput: %s", command, err, outputStr)
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "bash",
- 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: "bash",
- Status: "completed",
- Content: fmt.Sprintf("Output: %d lines", len(lines)),
- }
- }
-
- return result, nil
-}
-
-// Todo management tools
-type TodoItem struct {
- ID string `json:"id"`
- Content string `json:"content"`
- Status string `json:"status"` // pending, in_progress, completed
- Priority string `json:"priority"` // high, medium, low
-}
-
-var sessionTodos []TodoItem
-
-func (d *Del) todoRead(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "todo_read",
- Status: "reading",
- Content: "Reading todo list...",
- }
-
- if len(sessionTodos) == 0 {
- return "No todos in current session", nil
- }
-
- var result strings.Builder
- result.WriteString("Current Todo List:\n\n")
-
- for _, todo := range sessionTodos {
- status := "โณ"
- switch todo.Status {
- case "completed":
- status = "โ
"
- case "in_progress":
- status = "๐"
- }
-
- priority := ""
- switch todo.Priority {
- case "high":
- priority = " [HIGH]"
- case "medium":
- priority = " [MED]"
- case "low":
- priority = " [LOW]"
- }
-
- result.WriteString(fmt.Sprintf("%s %s%s\n", status, todo.Content, priority))
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "todo_read",
- Status: "completed",
- Content: fmt.Sprintf("Found %d todos", len(sessionTodos)),
- }
-
- return result.String(), nil
-}
-
-func (d *Del) todoWrite(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
- todosArg, ok := args["todos"]
- if !ok {
- return "", fmt.Errorf("missing todos argument")
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "todo_write",
- Status: "updating",
- Content: "Updating todo list...",
- }
-
- // Parse todos array
- todosArray, ok := todosArg.([]interface{})
- if !ok {
- return "", fmt.Errorf("todos must be an array")
- }
-
- var newTodos []TodoItem
- for i, todoArg := range todosArray {
- todoMap, ok := todoArg.(map[string]interface{})
- if !ok {
- return "", fmt.Errorf("todo %d must be an object", i)
- }
-
- todo := TodoItem{
- ID: todoMap["id"].(string),
- Content: todoMap["content"].(string),
- Status: todoMap["status"].(string),
- Priority: todoMap["priority"].(string),
- }
-
- newTodos = append(newTodos, todo)
- }
-
- sessionTodos = newTodos
-
- result := fmt.Sprintf("Updated todo list with %d items", len(newTodos))
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "todo_write",
- Status: "completed",
- Content: result,
- }
-
- return result, nil
-}
-
-func (d *Del) exitPlanMode(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
- plan, ok := args["plan"].(string)
- if !ok {
- return "", fmt.Errorf("missing plan argument")
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "exit_plan_mode",
- Status: "presenting",
- Content: "Presenting plan to user...",
- }
-
- result := fmt.Sprintf("Plan presented:\n\n%s\n\nReady to proceed with implementation?", plan)
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "exit_plan_mode",
- Status: "completed",
- Content: "Plan presented to user",
- }
-
- return result, nil
-}
-
-// Notebook tools (basic implementation)
-func (d *Del) notebookRead(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
- notebookPath, ok := args["notebook_path"].(string)
- if !ok {
- return "", fmt.Errorf("missing notebook_path argument")
- }
-
- cellID, _ := args["cell_id"].(string)
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "notebook_read",
- Status: "reading",
- Content: fmt.Sprintf("Reading notebook %s...", notebookPath),
- }
-
- data, err := os.ReadFile(notebookPath)
- if err != nil {
- return "", err
- }
-
- var notebook map[string]interface{}
- if err := json.Unmarshal(data, ¬ebook); err != nil {
- return "", fmt.Errorf("invalid notebook format: %v", err)
- }
-
- cells, ok := notebook["cells"].([]interface{})
- if !ok {
- return "", fmt.Errorf("notebook has no cells")
- }
-
- var result strings.Builder
- result.WriteString(fmt.Sprintf("Notebook: %s\n\n", notebookPath))
-
- for i, cellArg := range cells {
- cell, ok := cellArg.(map[string]interface{})
- if !ok {
- continue
- }
-
- id, _ := cell["id"].(string)
- cellType, _ := cell["cell_type"].(string)
-
- // If specific cell requested, only show that one
- if cellID != "" && id != cellID {
- continue
- }
-
- result.WriteString(fmt.Sprintf("Cell %d (%s):\n", i, cellType))
-
- source, ok := cell["source"].([]interface{})
- if ok {
- for _, line := range source {
- if lineStr, ok := line.(string); ok {
- result.WriteString(lineStr)
- }
- }
- }
- result.WriteString("\n---\n")
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "notebook_read",
- Status: "completed",
- Content: fmt.Sprintf("Read %d cells", len(cells)),
- }
-
- return result.String(), nil
-}
-
-func (d *Del) notebookEdit(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
- return "", fmt.Errorf("notebook editing not yet implemented - requires complex JSON manipulation")
-}
-
-// Web tools (basic implementation)
-func (d *Del) webFetch(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
- url, ok := args["url"].(string)
- if !ok {
- return "", fmt.Errorf("missing url argument")
- }
-
- promptArg, ok := args["prompt"].(string)
- if !ok {
- return "", fmt.Errorf("missing prompt argument")
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "web_fetch",
- Status: "fetching",
- Content: fmt.Sprintf("Fetching %s...", 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
- }
-
- content := string(body)
-
- // Basic HTML to text conversion (simplified)
- content = regexp.MustCompile(`<[^>]*>`).ReplaceAllString(content, "")
- content = strings.TrimSpace(content)
-
- result := fmt.Sprintf("Fetched content from %s:\n\nPrompt: %s\n\nContent:\n%s", url, promptArg, content)
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "web_fetch",
- Status: "completed",
- Content: fmt.Sprintf("Fetched %d characters", len(content)),
- }
-
- return result, nil
-}
-
-func (d *Del) webSearch(ctx context.Context, args map[string]interface{}, progress chan<- StreamMessage) (string, error) {
- query, ok := args["query"].(string)
- if !ok {
- return "", fmt.Errorf("missing query argument")
- }
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "web_search",
- Status: "searching",
- Content: fmt.Sprintf("Searching for '%s'...", query),
- }
-
- // Note: This is a placeholder - real web search requires API integration
- result := fmt.Sprintf("Web search for '%s':\n\nNote: Web search functionality requires API integration (Google, Bing, etc.)\nThis is a placeholder implementation.", query)
-
- progress <- StreamMessage{
- Type: MessageTypeProgress,
- ToolName: "web_search",
- Status: "completed",
- Content: "Search completed (placeholder)",
- }
-
- return result, nil
-}
-
-
-func (d *Del) executeTool(ctx context.Context, call ToolCall) string {
- startTime := time.Now()
-
- d.emit(StreamMessage{
- Type: MessageTypeTool,
- ToolName: call.Name,
- ToolArgs: call.Args,
- Status: "starting",
- })
-
- tool, exists := d.tools[call.Name]
- if !exists {
- 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)
- }
-
- 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
-
- 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: fmt.Sprintf("%s (took %s)", err.Error(), timeStr),
- })
- return err.Error()
- }
-
- d.emit(StreamMessage{
- Type: MessageTypeTool,
- ToolName: call.Name,
- Status: "completed",
- Result: result,
- Content: timeStr, // Store formatted timing in Content field
- })
-
- return result
-}
-
-func (d *Del) formatArgs(args map[string]interface{}) string {
- if len(args) == 0 {
- return ""
- }
-
- var parts []string
- for key, value := range args {
- if str, ok := value.(string); ok && len(str) > 30 {
- parts = append(parts, fmt.Sprintf("%s: \"%.30s...\"", key, str))
- } else {
- parts = append(parts, fmt.Sprintf("%s: %v", key, value))
- }
- }
- return strings.Join(parts, ", ")
-}
-
-func (d *Del) streamResponseChunks(ctx context.Context, text string) {
- // Instead of word-by-word streaming, send the full response for better markdown rendering
- d.emit(StreamMessage{
- Type: MessageTypeAssistant,
- Content: text,
- })
-}
-
-func (d *Del) buildOllamaTools() []api.Tool {
- var tools []api.Tool
-
- // === TESTING: MEMORY TOOLS ONLY FOR STABILITY ===
- // Test with just memory tools to see if that's more stable
-
- // Helper function to create property
- makeProperty := func(propType string, description string) struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- } {
- return struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- }{
- Type: api.PropertyType{propType},
- Description: description,
- }
- }
-
- // Memory tools only
- // remember tool
- rememberFunc := api.ToolFunction{
- Name: "remember",
- Description: "Store information in persistent memory",
- }
- rememberFunc.Parameters.Type = "object"
- rememberFunc.Parameters.Required = []string{"content"}
- rememberFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- rememberFunc.Parameters.Properties["content"] = makeProperty("string", "Information to remember")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: rememberFunc,
- })
-
- return tools
-
- // recall tool (temporarily disabled)
- recallFunc := api.ToolFunction{
- Name: "recall",
- Description: "Retrieve information from persistent memory",
- }
- recallFunc.Parameters.Type = "object"
- recallFunc.Parameters.Required = []string{}
- recallFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- recallFunc.Parameters.Properties["query"] = makeProperty("string", "Optional search query to filter memories")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: recallFunc,
- })
-
- return tools
-}
-
-// Original buildOllamaTools function starts here (now unused)
-func (d *Del) buildOllamaToolsOriginal() []api.Tool {
- var tools []api.Tool
-
- // Helper function to create property
- makeProperty := func(propType string, description string) struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- } {
- return struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- }{
- Type: api.PropertyType{propType},
- Description: description,
- }
- }
-
- // read_file tool
- readFileFunc := api.ToolFunction{
- Name: "read_file",
- Description: "Read file contents",
- }
- readFileFunc.Parameters.Type = "object"
- readFileFunc.Parameters.Required = []string{"path"}
- readFileFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- readFileFunc.Parameters.Properties["path"] = makeProperty("string", "Path to the file to read")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: readFileFunc,
- })
-
- // list_dir tool
- listDirFunc := api.ToolFunction{
- Name: "list_dir",
- Description: "List directory contents",
- }
- listDirFunc.Parameters.Type = "object"
- listDirFunc.Parameters.Required = []string{}
- listDirFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- listDirFunc.Parameters.Properties["path"] = makeProperty("string", "Path to the directory to list (defaults to current directory)")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: listDirFunc,
- })
-
- // run_command tool
- runCommandFunc := api.ToolFunction{
- Name: "run_command",
- Description: "Execute shell commands",
- }
- runCommandFunc.Parameters.Type = "object"
- runCommandFunc.Parameters.Required = []string{"command"}
- runCommandFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- runCommandFunc.Parameters.Properties["command"] = makeProperty("string", "Shell command to execute")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: runCommandFunc,
- })
-
- // === TEMPORARY: MINIMAL TOOL SET FOR DEBUGGING ===
- // Reduced from 22 tools to 3 essential tools to fix hanging issue
- return tools
-
- // git_status tool
- gitStatusFunc := api.ToolFunction{
- Name: "git_status",
- Description: "Check git repository status",
- }
- gitStatusFunc.Parameters.Type = "object"
- gitStatusFunc.Parameters.Required = []string{}
- gitStatusFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: gitStatusFunc,
- })
-
- // write_file tool
- writeFileFunc := api.ToolFunction{
- Name: "write_file",
- Description: "Write content to files",
- }
- writeFileFunc.Parameters.Type = "object"
- writeFileFunc.Parameters.Required = []string{"path", "content"}
- writeFileFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- writeFileFunc.Parameters.Properties["path"] = makeProperty("string", "Path to the file to write")
- writeFileFunc.Parameters.Properties["content"] = makeProperty("string", "Content to write to the file")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: writeFileFunc,
- })
-
- // analyze_code tool
- analyzeCodeFunc := api.ToolFunction{
- Name: "analyze_code",
- Description: "Analyze code structure (auto-detects files if no path provided)",
- }
- analyzeCodeFunc.Parameters.Type = "object"
- analyzeCodeFunc.Parameters.Required = []string{}
- analyzeCodeFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- analyzeCodeFunc.Parameters.Properties["path"] = makeProperty("string", "Path to the file to analyze (optional, auto-detects if not provided)")
- analyzeCodeFunc.Parameters.Properties["content"] = makeProperty("string", "Code content to analyze (optional)")
- analyzeCodeFunc.Parameters.Properties["language"] = makeProperty("string", "Programming language (optional, auto-detected from file extension)")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: analyzeCodeFunc,
- })
-
- // search_code tool
- searchCodeFunc := api.ToolFunction{
- Name: "search_code",
- Description: "Search for patterns in code",
- }
- searchCodeFunc.Parameters.Type = "object"
- searchCodeFunc.Parameters.Required = []string{"pattern"}
- searchCodeFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- searchCodeFunc.Parameters.Properties["pattern"] = makeProperty("string", "Pattern to search for")
- searchCodeFunc.Parameters.Properties["path"] = makeProperty("string", "Path to search in (defaults to current directory)")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: searchCodeFunc,
- })
-
- // edit_file tool
- editFileFunc := api.ToolFunction{
- Name: "edit_file",
- Description: "Performs exact string replacements in files",
- }
- editFileFunc.Parameters.Type = "object"
- editFileFunc.Parameters.Required = []string{"file_path", "old_string", "new_string"}
- editFileFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- editFileFunc.Parameters.Properties["file_path"] = makeProperty("string", "Absolute path to the file to edit")
- editFileFunc.Parameters.Properties["old_string"] = makeProperty("string", "Text to replace")
- editFileFunc.Parameters.Properties["new_string"] = makeProperty("string", "Replacement text")
- editFileFunc.Parameters.Properties["replace_all"] = makeProperty("boolean", "Replace all occurrences (default: false)")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: editFileFunc,
- })
-
- // multi_edit tool
- multiEditFunc := api.ToolFunction{
- Name: "multi_edit",
- Description: "Multiple edits to a single file in one operation",
- }
- multiEditFunc.Parameters.Type = "object"
- multiEditFunc.Parameters.Required = []string{"file_path", "edits"}
- multiEditFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- multiEditFunc.Parameters.Properties["file_path"] = makeProperty("string", "Absolute path to the file to edit")
- multiEditFunc.Parameters.Properties["edits"] = makeProperty("array", "Array of edit operations to perform sequentially")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: multiEditFunc,
- })
-
- // glob tool
- globFunc := api.ToolFunction{
- Name: "glob",
- Description: "Fast file pattern matching tool",
- }
- globFunc.Parameters.Type = "object"
- globFunc.Parameters.Required = []string{"pattern"}
- globFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- globFunc.Parameters.Properties["pattern"] = makeProperty("string", "Glob pattern to match files (e.g., '**/*.js')")
- globFunc.Parameters.Properties["path"] = makeProperty("string", "Directory to search in (defaults to current directory)")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: globFunc,
- })
-
- // grep tool
- grepFunc := api.ToolFunction{
- Name: "grep",
- Description: "Fast content search using regular expressions",
- }
- grepFunc.Parameters.Type = "object"
- grepFunc.Parameters.Required = []string{"pattern"}
- grepFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- grepFunc.Parameters.Properties["pattern"] = makeProperty("string", "Regular expression pattern to search for")
- grepFunc.Parameters.Properties["path"] = makeProperty("string", "Directory to search in (defaults to current directory)")
- grepFunc.Parameters.Properties["include"] = makeProperty("string", "File pattern to include in search (e.g., '*.js')")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: grepFunc,
- })
-
- // bash tool (enhanced)
- bashFunc := api.ToolFunction{
- Name: "bash",
- Description: "Execute bash commands with timeout and security measures",
- }
- bashFunc.Parameters.Type = "object"
- bashFunc.Parameters.Required = []string{"command"}
- bashFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- bashFunc.Parameters.Properties["command"] = makeProperty("string", "Bash command to execute")
- bashFunc.Parameters.Properties["timeout"] = makeProperty("number", "Timeout in milliseconds (max 600000ms)")
- bashFunc.Parameters.Properties["description"] = makeProperty("string", "5-10 word description of what the command does")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: bashFunc,
- })
-
- // todo_read tool
- todoReadFunc := api.ToolFunction{
- Name: "todo_read",
- Description: "Read the current todo list for the session",
- }
- todoReadFunc.Parameters.Type = "object"
- todoReadFunc.Parameters.Required = []string{}
- todoReadFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: todoReadFunc,
- })
-
- // todo_write tool
- todoWriteFunc := api.ToolFunction{
- Name: "todo_write",
- Description: "Create and manage structured task list",
- }
- todoWriteFunc.Parameters.Type = "object"
- todoWriteFunc.Parameters.Required = []string{"todos"}
- todoWriteFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- todoWriteFunc.Parameters.Properties["todos"] = makeProperty("array", "Array of todo items with id, content, status, and priority")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: todoWriteFunc,
- })
-
- // exit_plan_mode tool
- exitPlanModeFunc := api.ToolFunction{
- Name: "exit_plan_mode",
- Description: "Exit plan mode after presenting plan to user",
- }
- exitPlanModeFunc.Parameters.Type = "object"
- exitPlanModeFunc.Parameters.Required = []string{"plan"}
- exitPlanModeFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- exitPlanModeFunc.Parameters.Properties["plan"] = makeProperty("string", "Concise plan with markdown support")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: exitPlanModeFunc,
- })
-
- // notebook_read tool
- notebookReadFunc := api.ToolFunction{
- Name: "notebook_read",
- Description: "Read Jupyter notebook cells and outputs",
- }
- notebookReadFunc.Parameters.Type = "object"
- notebookReadFunc.Parameters.Required = []string{"notebook_path"}
- notebookReadFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- notebookReadFunc.Parameters.Properties["notebook_path"] = makeProperty("string", "Absolute path to .ipynb file")
- notebookReadFunc.Parameters.Properties["cell_id"] = makeProperty("string", "Specific cell ID to read (optional)")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: notebookReadFunc,
- })
-
- // notebook_edit tool
- notebookEditFunc := api.ToolFunction{
- Name: "notebook_edit",
- Description: "Edit Jupyter notebook cell contents",
- }
- notebookEditFunc.Parameters.Type = "object"
- notebookEditFunc.Parameters.Required = []string{"notebook_path", "cell_id", "new_source"}
- notebookEditFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- notebookEditFunc.Parameters.Properties["notebook_path"] = makeProperty("string", "Absolute path to .ipynb file")
- notebookEditFunc.Parameters.Properties["cell_id"] = makeProperty("string", "Cell ID to edit")
- notebookEditFunc.Parameters.Properties["new_source"] = makeProperty("string", "New cell content")
- notebookEditFunc.Parameters.Properties["cell_type"] = makeProperty("string", "Cell type: 'code' or 'markdown'")
- notebookEditFunc.Parameters.Properties["edit_mode"] = makeProperty("string", "Edit mode: 'replace', 'insert', or 'delete'")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: notebookEditFunc,
- })
-
- // web_fetch tool
- webFetchFunc := api.ToolFunction{
- Name: "web_fetch",
- Description: "Fetch and process web content",
- }
- webFetchFunc.Parameters.Type = "object"
- webFetchFunc.Parameters.Required = []string{"url", "prompt"}
- webFetchFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- webFetchFunc.Parameters.Properties["url"] = makeProperty("string", "URL to fetch content from")
- webFetchFunc.Parameters.Properties["prompt"] = makeProperty("string", "Processing prompt for the fetched content")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: webFetchFunc,
- })
-
- // web_search tool
- webSearchFunc := api.ToolFunction{
- Name: "web_search",
- Description: "Search the web for current information",
- }
- webSearchFunc.Parameters.Type = "object"
- webSearchFunc.Parameters.Required = []string{"query"}
- webSearchFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- webSearchFunc.Parameters.Properties["query"] = makeProperty("string", "Search query")
- webSearchFunc.Parameters.Properties["allowed_domains"] = makeProperty("array", "Domain whitelist")
- webSearchFunc.Parameters.Properties["blocked_domains"] = makeProperty("array", "Domain blacklist")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: webSearchFunc,
- })
-
- // remember tool
- rememberFunc := api.ToolFunction{
- Name: "remember",
- Description: "Store information in persistent memory for future recall",
- }
- rememberFunc.Parameters.Type = "object"
- rememberFunc.Parameters.Required = []string{"content"}
- rememberFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- rememberFunc.Parameters.Properties["content"] = makeProperty("string", "Information to remember")
- rememberFunc.Parameters.Properties["entity"] = makeProperty("string", "Optional entity/category for organization")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: rememberFunc,
- })
-
- // recall tool
- recallFunc := api.ToolFunction{
- Name: "recall",
- Description: "Search and retrieve information from persistent memory",
- }
- recallFunc.Parameters.Type = "object"
- recallFunc.Parameters.Required = []string{"query"}
- recallFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- recallFunc.Parameters.Properties["query"] = makeProperty("string", "Search query for memory recall")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: recallFunc,
- })
-
- // forget tool
- forgetFunc := api.ToolFunction{
- Name: "forget",
- Description: "Delete specific information from persistent memory",
- }
- forgetFunc.Parameters.Type = "object"
- forgetFunc.Parameters.Required = []string{"entity"}
- forgetFunc.Parameters.Properties = make(map[string]struct {
- Type api.PropertyType `json:"type"`
- Items any `json:"items,omitempty"`
- Description string `json:"description"`
- Enum []any `json:"enum,omitempty"`
- })
- forgetFunc.Parameters.Properties["entity"] = makeProperty("string", "Entity/memory to delete")
-
- tools = append(tools, api.Tool{
- Type: "function",
- Function: forgetFunc,
- })
-
- return tools
-}
-
-// Fallback text parsing for models without native tool support
-func (d *Del) parseTextToolCalls(input string) []ToolCall {
- var calls []ToolCall
-
- // Simple command detection based on user input
- originalInput := input
- input = strings.ToLower(strings.TrimSpace(input))
-
- // File operations
- if input == "list files" || input == "list the files" || input == "ls" {
- calls = append(calls, ToolCall{Name: "list_dir", Args: map[string]interface{}{}})
- } else if strings.HasPrefix(strings.ToLower(originalInput), "read ") {
- // Extract filename from original input to preserve case
- filename := originalInput[5:] // Skip "read " or "Read " etc.
- filename = strings.TrimSpace(filename)
- if filename != "" {
- calls = append(calls, ToolCall{Name: "read_file", Args: map[string]interface{}{"path": filename}})
- }
- } else if strings.HasPrefix(input, "write ") {
- // Basic write file detection - would need more sophisticated parsing for real use
- parts := strings.Fields(originalInput[6:]) // Skip "write "
- if len(parts) >= 1 {
- calls = append(calls, ToolCall{Name: "write_file", Args: map[string]interface{}{
- "path": parts[0],
- "content": "# Content would need to be specified in a more sophisticated way",
- }})
- }
- } else if strings.HasPrefix(strings.ToLower(originalInput), "edit ") {
- // Basic edit detection - preserve case in filename
- filename := originalInput[5:] // Skip "edit " or "Edit " etc.
- filename = strings.TrimSpace(filename)
- if filename != "" {
- calls = append(calls, ToolCall{Name: "edit_file", Args: map[string]interface{}{
- "file_path": filename,
- "old_string": "# Would need more sophisticated parsing",
- "new_string": "# Would need more sophisticated parsing",
- }})
- }
-
- // Search operations
- } else if strings.HasPrefix(input, "find ") || strings.HasPrefix(input, "glob ") {
- pattern := strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(input, "find "), "glob "))
- if pattern != "" {
- calls = append(calls, ToolCall{Name: "glob", Args: map[string]interface{}{"pattern": pattern}})
- }
- } else if strings.HasPrefix(input, "grep ") {
- pattern := strings.TrimPrefix(input, "grep ")
- pattern = strings.TrimSpace(pattern)
- if pattern != "" {
- calls = append(calls, ToolCall{Name: "grep", Args: map[string]interface{}{"pattern": pattern}})
- }
- } else if strings.HasPrefix(input, "search ") {
- // Extract search pattern
- pattern := strings.TrimPrefix(input, "search ")
- pattern = strings.TrimSpace(pattern)
- if pattern != "" {
- calls = append(calls, ToolCall{Name: "search_code", Args: map[string]interface{}{"pattern": pattern}})
- }
-
- // Command execution
- } else if strings.HasPrefix(input, "run ") || strings.HasPrefix(input, "bash ") || strings.HasPrefix(input, "execute ") {
- // Extract command
- var command string
- if strings.HasPrefix(input, "run ") {
- command = strings.TrimPrefix(originalInput, "run ")
- } else if strings.HasPrefix(input, "bash ") {
- command = strings.TrimPrefix(originalInput, "bash ")
- } else {
- command = strings.TrimPrefix(originalInput, "execute ")
- }
- command = strings.TrimSpace(command)
- if command != "" {
- calls = append(calls, ToolCall{Name: "bash", Args: map[string]interface{}{"command": command}})
- }
-
- // Git operations
- } else if input == "git status" || input == "check git status" || input == "check git" {
- calls = append(calls, ToolCall{Name: "git_status", Args: map[string]interface{}{}})
-
- // Code analysis
- } else if input == "analyze the code" || input == "analyze code" || input == "analyze this project" {
- calls = append(calls, ToolCall{Name: "analyze_code", Args: map[string]interface{}{}})
-
- // Todo management
- } else if input == "show todos" || input == "list todos" || input == "read todos" {
- calls = append(calls, ToolCall{Name: "todo_read", Args: map[string]interface{}{}})
-
- // Web operations
- } else if strings.HasPrefix(input, "fetch ") {
- url := strings.TrimPrefix(input, "fetch ")
- url = strings.TrimSpace(url)
- if url != "" {
- calls = append(calls, ToolCall{Name: "web_fetch", Args: map[string]interface{}{
- "url": url,
- "prompt": "Summarize this web page content",
- }})
- }
- } else if strings.HasPrefix(input, "web search ") || strings.HasPrefix(input, "websearch ") {
- query := strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(input, "web search "), "websearch "))
- if query != "" {
- calls = append(calls, ToolCall{Name: "web_search", Args: map[string]interface{}{"query": query}})
- }
-
- // Notebook operations
- } else if strings.HasPrefix(strings.ToLower(originalInput), "read notebook ") {
- notebook := originalInput[14:] // Skip "read notebook " or "Read Notebook " etc.
- notebook = strings.TrimSpace(notebook)
- if notebook != "" {
- calls = append(calls, ToolCall{Name: "notebook_read", Args: map[string]interface{}{"notebook_path": notebook}})
- }
- }
-
- return calls
-}
-
-func (d *Del) processMessage(ctx context.Context, userInput string) {
- d.emit(StreamMessage{
- Type: MessageTypeUser,
- Content: userInput,
- })
-
- d.chatHistory = append(d.chatHistory, api.Message{Role: "user", Content: userInput})
-
- // Start thinking indicator
- d.startThinking("๐ค Analyzing your request...")
- d.updateThinking("๐ง Processing with AI model and tools...")
-
- // Create context with timeout
- chatCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
- defer cancel()
-
- // Build tools for Ollama
- tools := d.buildOllamaTools()
-
- var fullResponse string
- var toolCalls []api.ToolCall
-
- err := d.client.Chat(chatCtx, &api.ChatRequest{
- Model: d.model,
- Messages: d.chatHistory,
- Tools: tools,
- }, func(resp api.ChatResponse) error {
- // Handle streaming response
- if resp.Message.Content != "" {
- fullResponse += resp.Message.Content
- }
-
- // Handle tool calls
- if len(resp.Message.ToolCalls) > 0 {
- toolCalls = append(toolCalls, resp.Message.ToolCalls...)
- }
-
- return nil
- })
-
- if err != nil {
- d.stopThinking()
- d.emit(StreamMessage{
- Type: MessageTypeSystem,
- Error: fmt.Sprintf("Chat error: %v", err),
- })
- return
- }
-
- // Add assistant message to history
- d.chatHistory = append(d.chatHistory, api.Message{
- Role: "assistant",
- Content: fullResponse,
- ToolCalls: toolCalls,
- })
-
- // Execute tool calls if any
- if len(toolCalls) > 0 {
- d.updateThinking(fmt.Sprintf("๐ง Executing %d tool(s)...", len(toolCalls)))
-
- var toolResults []api.Message
- for _, toolCall := range toolCalls {
- d.updateThinking(fmt.Sprintf("โก Running %s...", toolCall.Function.Name))
-
- // Convert Ollama tool call to our format
- call := ToolCall{
- Name: toolCall.Function.Name,
- Args: toolCall.Function.Arguments,
- }
-
- result := d.executeTool(ctx, call)
-
- // Add tool result to chat history
- toolResults = append(toolResults, api.Message{
- Role: "tool",
- Content: result,
- ToolCalls: []api.ToolCall{{
- Function: api.ToolCallFunction{
- Name: toolCall.Function.Name,
- },
- }},
- })
- }
-
- // Add all tool results to history
- d.chatHistory = append(d.chatHistory, toolResults...)
-
- // Get final AI response after tool execution with simplified history
- d.updateThinking("๐ง Generating final response...")
-
- // Create simplified chat history for final response (avoid complex tool structures)
- simplifiedHistory := []api.Message{
- {Role: "user", Content: userInput},
- }
-
- // Add a summary of tool execution results instead of raw tool data
- var toolSummary strings.Builder
- toolSummary.WriteString("I executed the following tools:\n")
- for _, toolCall := range toolCalls {
- toolSummary.WriteString(fmt.Sprintf("- %s: completed successfully\n", toolCall.Function.Name))
- }
- toolSummary.WriteString("\nPlease provide a helpful response based on the tool execution.")
-
- simplifiedHistory = append(simplifiedHistory, api.Message{
- Role: "assistant",
- Content: toolSummary.String(),
- })
-
- finalCtx, finalCancel := context.WithTimeout(ctx, 15*time.Second) // Reduced timeout
- defer finalCancel()
-
- var finalResponse string
- err = d.client.Chat(finalCtx, &api.ChatRequest{
- Model: d.model,
- Messages: simplifiedHistory,
- // Don't include tools in final response to avoid infinite loops
- }, func(resp api.ChatResponse) error {
- finalResponse += resp.Message.Content
- return nil
- })
-
- if err == nil && strings.TrimSpace(finalResponse) != "" {
- d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: finalResponse})
- fullResponse = finalResponse
- } else {
- // If final response fails or is empty, provide a helpful fallback
- if err != nil {
- d.updateThinking(fmt.Sprintf("โ ๏ธ Final response failed: %v", err))
- }
- fullResponse = "โ
Tool execution completed successfully."
- }
- }
-
- d.stopThinking()
-
- // Stream the final response
- if fullResponse != "" {
- d.streamResponseChunks(ctx, fullResponse)
- } else {
- d.emit(StreamMessage{
- Type: MessageTypeAssistant,
- Content: "โ
Task completed successfully.",
- })
- }
-}
-
-func (d *Del) renderUI() {
- currentLine := ""
-
- 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.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 = ""
- }
- }
-
- case MessageTypeAssistant:
- // Clear any thinking indicator
- if currentLine != "" {
- fmt.Print("\r\033[K")
- currentLine = ""
- }
-
- fmt.Print("๐ค Del: ")
-
- // Render markdown content
- rendered := renderMarkdown(msg.Content)
-
- // Indent the content to align with the prefix
- lines := strings.Split(rendered, "\n")
- for i, line := range lines {
- if i == 0 {
- fmt.Println(line)
- } else {
- fmt.Printf(" %s\n", line) // 8 spaces to align with "๐ค Del: "
- }
- }
-
- case MessageTypeTool:
- // Clear any thinking indicator
- if currentLine != "" {
- fmt.Print("\r\033[K")
- 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 != "" {
- timing := ""
- if msg.Content != "" {
- timing = fmt.Sprintf(" (โฑ๏ธ %s)", msg.Content)
- }
- // Always show the full result, but format it nicely
- fmt.Printf(" โฟ %s%s\n", strings.ReplaceAll(msg.Result, "\n", "\n "), timing)
- }
-
- 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)
- } else if msg.Content != "" {
- fmt.Printf("โน๏ธ %s\n", msg.Content)
- }
- }
- }
-}
-
-func (d *Del) Start(ctx context.Context) {
- cwd, _ := os.Getwd()
- 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: ")
- if !scanner.Scan() {
- break
- }
-
- input := strings.TrimSpace(scanner.Text())
- if input == "" {
- continue
- }
-
- if input == "quit" || input == "exit" || input == "q" {
- fmt.Println("๐ Stay funky!")
- d.cleanup()
- 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()
- }
-
- close(d.output)
-}
-
-func (d *Del) cleanup() {
- // No cleanup needed for exec-based MCP calls
-}
-
-func main() {
- var model = flag.String("model", "qwen2.5:latest", "Ollama model to use")
- var help = flag.Bool("help", false, "Show help message")
-
- flag.Parse()
-
- if *help {
- fmt.Println(`๐ค Del the Funky Robosapien - Claude Code Style AI Assistant
-
-Usage:
- del [flags]
-
-Flags:
- --model string Ollama model to use (default: qwen2.5:latest)
- --help Show this help message
-
-Popular Models:
- qwen2.5:latest # Best for coding with tools (default)
- mistral:latest # General purpose model
- codellama:7b # Meta's coding model (local)
-
-Examples:
- del # Use default model (qwen2.5)
- del --model mistral:latest # Use Mistral
-
-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
+ // Check if stdout is a terminal
+ if fileInfo, _ := os.Stdout.Stat(); (fileInfo.Mode() & os.ModeCharDevice) == 0 {
+ return false
}
- ctx := context.Background()
- assistant := NewDel(*model)
- assistant.Start(ctx)
+ return true
}
\ No newline at end of file
go.mod
@@ -5,7 +5,42 @@ go 1.24.0
require github.com/ollama/ollama v0.9.2
require (
+ github.com/alecthomas/chroma/v2 v2.14.0 // indirect
+ github.com/atotto/clipboard v0.1.4 // indirect
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/aymerick/douceur v0.2.0 // indirect
+ github.com/charmbracelet/bubbles v0.21.0 // indirect
+ github.com/charmbracelet/bubbletea v1.3.5 // indirect
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
+ github.com/charmbracelet/glamour v0.10.0 // indirect
+ github.com/charmbracelet/harmonica v0.2.0 // indirect
+ github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
+ github.com/charmbracelet/x/ansi v0.8.0 // indirect
+ github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
+ github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
+ github.com/charmbracelet/x/term v0.2.1 // indirect
+ github.com/dlclark/regexp2 v1.11.4 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+ github.com/gorilla/css v1.0.1 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-localereader v0.0.1 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
+ github.com/microcosm-cc/bluemonday v1.0.27 // indirect
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/reflow v0.3.0 // indirect
+ github.com/muesli/termenv v0.16.0 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/sahilm/fuzzy v0.1.1 // indirect
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ github.com/yuin/goldmark v1.7.8 // indirect
+ github.com/yuin/goldmark-emoji v1.0.5 // indirect
golang.org/x/crypto v0.36.0 // indirect
+ golang.org/x/net v0.38.0 // indirect
+ golang.org/x/sync v0.13.0 // indirect
golang.org/x/sys v0.32.0 // indirect
golang.org/x/term v0.31.0 // indirect
+ golang.org/x/text v0.24.0 // indirect
)
go.sum
@@ -1,18 +1,100 @@
+github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
+github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
+github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
+github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
+github.com/charmbracelet/bubbletea v1.3.5 h1:JAMNLTbqMOhSwoELIr0qyP4VidFq72/6E9j7HHmRKQc=
+github.com/charmbracelet/bubbletea v1.3.5/go.mod h1:TkCnmH+aBd4LrXhXcqrKiYwRs7qyQx5rBgH5fVY3v54=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
+github.com/charmbracelet/glamour v0.10.0 h1:41/IYxsmIpaBjkMXjrjLwsHDBlucd5at6tY5n2r/qn4=
+github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
+github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
+github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
+github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
+github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
+github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
+github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
+github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
+github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
+github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
+github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
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/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
+github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
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/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
+github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
+github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
+github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
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/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
+github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
+github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
+github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
+golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
+golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=