diff options
| author | Kujtim Hoxha <[email protected]> | 2025-04-03 19:50:31 +0200 |
|---|---|---|
| committer | Kujtim Hoxha <[email protected]> | 2025-04-03 19:50:31 +0200 |
| commit | 49b593d9ddbe278358b30dcde444932172bea615 (patch) | |
| tree | dbe6c779ca4932ff369f3684dbefd3e0e599e0fd /internal | |
| parent | 230917bbbf5fa2cbdb37f298554889a7a5233744 (diff) | |
| download | opencode-49b593d9ddbe278358b30dcde444932172bea615.tar.gz opencode-49b593d9ddbe278358b30dcde444932172bea615.zip | |
small fixes
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/llm/agent/coder.go | 2 | ||||
| -rw-r--r-- | internal/llm/tools/bash.go | 4 | ||||
| -rw-r--r-- | internal/llm/tools/edit.go | 7 | ||||
| -rw-r--r-- | internal/llm/tools/fetch.go | 223 | ||||
| -rw-r--r-- | internal/tui/components/dialog/permission.go | 17 |
5 files changed, 246 insertions, 7 deletions
diff --git a/internal/llm/agent/coder.go b/internal/llm/agent/coder.go index d167ede99..b47289c33 100644 --- a/internal/llm/agent/coder.go +++ b/internal/llm/agent/coder.go @@ -55,9 +55,11 @@ func NewCoderAgent(app *app.App) (Agent, error) { []tools.BaseTool{ tools.NewBashTool(), tools.NewEditTool(app.LSPClients), + tools.NewFetchTool(), tools.NewGlobTool(), tools.NewGrepTool(), tools.NewLsTool(), + tools.NewSourcegraphTool(), tools.NewViewTool(app.LSPClients), tools.NewWriteTool(app.LSPClients), }, otherTools..., diff --git a/internal/llm/tools/bash.go b/internal/llm/tools/bash.go index 0b9e13b85..1481bdd88 100644 --- a/internal/llm/tools/bash.go +++ b/internal/llm/tools/bash.go @@ -97,7 +97,9 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) ToolName: BashToolName, Action: "execute", Description: fmt.Sprintf("Execute command: %s", params.Command), - Params: BashPermissionsParams(params), + Params: BashPermissionsParams{ + Command: params.Command, + }, }, ) if !p { diff --git a/internal/llm/tools/edit.go b/internal/llm/tools/edit.go index a5877044b..f12a1eb21 100644 --- a/internal/llm/tools/edit.go +++ b/internal/llm/tools/edit.go @@ -260,7 +260,12 @@ func replaceContent(filePath, oldString, newString string) (string, error) { } newContent := oldContent[:index] + newString + oldContent[index+len(oldString):] - diff := GenerateDiff(oldString, newContent) + + startIndex := max(0, index-3) + oldEndIndex := min(len(oldContent), index+len(oldString)+3) + newEndIndex := min(len(newContent), index+len(newString)+3) + + diff := GenerateDiff(oldContent[startIndex:oldEndIndex], newContent[startIndex:newEndIndex]) p := permission.Default.Request( permission.CreatePermissionRequest{ diff --git a/internal/llm/tools/fetch.go b/internal/llm/tools/fetch.go new file mode 100644 index 000000000..0a852626c --- /dev/null +++ b/internal/llm/tools/fetch.go @@ -0,0 +1,223 @@ +package tools + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + md "github.com/JohannesKaufmann/html-to-markdown" + "github.com/PuerkitoBio/goquery" + "github.com/kujtimiihoxha/termai/internal/config" + "github.com/kujtimiihoxha/termai/internal/permission" +) + +const ( + FetchToolName = "fetch" + fetchToolDescription = `Fetches content from a URL and returns it in the specified format. + +WHEN TO USE THIS TOOL: +- Use when you need to download content from a URL +- Helpful for retrieving documentation, API responses, or web content +- Useful for getting external information to assist with tasks + +HOW TO USE: +- Provide the URL to fetch content from +- Specify the desired output format (text, markdown, or html) +- Optionally set a timeout for the request + +FEATURES: +- Supports three output formats: text, markdown, and html +- Automatically handles HTTP redirects +- Sets reasonable timeouts to prevent hanging +- Validates input parameters before making requests + +LIMITATIONS: +- Maximum response size is 5MB +- Only supports HTTP and HTTPS protocols +- Cannot handle authentication or cookies +- Some websites may block automated requests + +TIPS: +- Use text format for plain text content or simple API responses +- Use markdown format for content that should be rendered with formatting +- Use html format when you need the raw HTML structure +- Set appropriate timeouts for potentially slow websites` +) + +type FetchParams struct { + URL string `json:"url"` + Format string `json:"format"` + Timeout int `json:"timeout,omitempty"` +} + +type FetchPermissionsParams struct { + URL string `json:"url"` + Format string `json:"format"` + Timeout int `json:"timeout,omitempty"` +} + +type fetchTool struct { + client *http.Client +} + +func NewFetchTool() BaseTool { + return &fetchTool{ + client: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +func (t *fetchTool) Info() ToolInfo { + return ToolInfo{ + Name: FetchToolName, + Description: fetchToolDescription, + Parameters: map[string]any{ + "url": map[string]any{ + "type": "string", + "description": "The URL to fetch content from", + }, + "format": map[string]any{ + "type": "string", + "description": "The format to return the content in (text, markdown, or html)", + }, + "timeout": map[string]any{ + "type": "number", + "description": "Optional timeout in seconds (max 120)", + }, + }, + Required: []string{"url", "format"}, + } +} + +func (t *fetchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) { + var params FetchParams + if err := json.Unmarshal([]byte(call.Input), ¶ms); err != nil { + return NewTextErrorResponse("Failed to parse fetch parameters: " + err.Error()), nil + } + + if params.URL == "" { + return NewTextErrorResponse("URL parameter is required"), nil + } + + format := strings.ToLower(params.Format) + if format != "text" && format != "markdown" && format != "html" { + return NewTextErrorResponse("Format must be one of: text, markdown, html"), nil + } + + if !strings.HasPrefix(params.URL, "http://") && !strings.HasPrefix(params.URL, "https://") { + return NewTextErrorResponse("URL must start with http:// or https://"), nil + } + + p := permission.Default.Request( + permission.CreatePermissionRequest{ + Path: config.WorkingDirectory(), + ToolName: FetchToolName, + Action: "fetch", + Description: fmt.Sprintf("Fetch content from URL: %s", params.URL), + Params: FetchPermissionsParams{ + URL: params.URL, + Format: params.Format, + Timeout: params.Timeout, + }, + }, + ) + + if !p { + return NewTextErrorResponse("Permission denied to fetch from URL: " + params.URL), nil + } + + client := t.client + if params.Timeout > 0 { + maxTimeout := 120 // 2 minutes + if params.Timeout > maxTimeout { + params.Timeout = maxTimeout + } + client = &http.Client{ + Timeout: time.Duration(params.Timeout) * time.Second, + } + } + + req, err := http.NewRequestWithContext(ctx, "GET", params.URL, nil) + if err != nil { + return NewTextErrorResponse("Failed to create request: " + err.Error()), nil + } + + req.Header.Set("User-Agent", "termai/1.0") + + resp, err := client.Do(req) + if err != nil { + return NewTextErrorResponse("Failed to execute request: " + err.Error()), nil + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil + } + + maxSize := int64(5 * 1024 * 1024) // 5MB + body, err := io.ReadAll(io.LimitReader(resp.Body, maxSize)) + if err != nil { + return NewTextErrorResponse("Failed to read response body: " + err.Error()), nil + } + + content := string(body) + contentType := resp.Header.Get("Content-Type") + + switch format { + case "text": + if strings.Contains(contentType, "text/html") { + text, err := extractTextFromHTML(content) + if err != nil { + return NewTextErrorResponse("Failed to extract text from HTML: " + err.Error()), nil + } + return NewTextResponse(text), nil + } + return NewTextResponse(content), nil + + case "markdown": + if strings.Contains(contentType, "text/html") { + markdown, err := convertHTMLToMarkdown(content) + if err != nil { + return NewTextErrorResponse("Failed to convert HTML to Markdown: " + err.Error()), nil + } + return NewTextResponse(markdown), nil + } + + return NewTextResponse("```\n" + content + "\n```"), nil + + case "html": + return NewTextResponse(content), nil + + default: + return NewTextResponse(content), nil + } +} + +func extractTextFromHTML(html string) (string, error) { + doc, err := goquery.NewDocumentFromReader(strings.NewReader(html)) + if err != nil { + return "", err + } + + text := doc.Text() + text = strings.Join(strings.Fields(text), " ") + + return text, nil +} + +func convertHTMLToMarkdown(html string) (string, error) { + converter := md.NewConverter("", true, nil) + + markdown, err := converter.ConvertString(html) + if err != nil { + return "", err + } + + return markdown, nil +} + diff --git a/internal/tui/components/dialog/permission.go b/internal/tui/components/dialog/permission.go index 7dcca9bae..29f1ff709 100644 --- a/internal/tui/components/dialog/permission.go +++ b/internal/tui/components/dialog/permission.go @@ -112,10 +112,11 @@ func (p *permissionDialogCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } func (p *permissionDialogCmp) render() string { - form := p.form.View() keyStyle := lipgloss.NewStyle().Bold(true).Foreground(styles.Rosewater) valueStyle := lipgloss.NewStyle().Foreground(styles.Peach) + form := p.form.View() + headerParts := []string{ lipgloss.JoinHorizontal(lipgloss.Left, keyStyle.Render("Tool:"), " ", valueStyle.Render(p.permission.ToolName)), " ", @@ -135,12 +136,15 @@ func (p *permissionDialogCmp) render() string { content, _ = r.Render(fmt.Sprintf("```bash\n%s\n```", pr.Command)) case tools.EditToolName: pr := p.permission.Params.(tools.EditPermissionsParams) - headerParts = append(headerParts, keyStyle.Render("Update:")) + headerParts = append(headerParts, keyStyle.Render("Update")) content, _ = r.Render(fmt.Sprintf("```diff\n%s\n```", pr.Diff)) case tools.WriteToolName: pr := p.permission.Params.(tools.WritePermissionsParams) - headerParts = append(headerParts, keyStyle.Render("Content:")) + headerParts = append(headerParts, keyStyle.Render("Content")) content, _ = r.Render(fmt.Sprintf("```diff\n%s\n```", pr.Content)) + case tools.FetchToolName: + pr := p.permission.Params.(tools.FetchPermissionsParams) + headerParts = append(headerParts, keyStyle.Render("URL: "+pr.URL)) default: content, _ = r.Render(p.permission.Description) } @@ -153,11 +157,14 @@ func (p *permissionDialogCmp) render() string { contentBorder = lipgloss.DoubleBorder() } cotentStyle := lipgloss.NewStyle().MarginTop(1).Padding(0, 1).Border(contentBorder).BorderForeground(styles.Flamingo) - + contentFinal := cotentStyle.Render(p.contentViewPort.View()) + if content == "" { + contentFinal = "" + } return lipgloss.JoinVertical( lipgloss.Top, headerContent, - cotentStyle.Render(p.contentViewPort.View()), + contentFinal, form, ) } |
