small fixes

This commit is contained in:
Kujtim Hoxha
2025-04-03 19:50:31 +02:00
parent 230917bbbf
commit 49b593d9dd
8 changed files with 331 additions and 11 deletions

View File

@@ -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...,

View File

@@ -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 {

View File

@@ -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{

223
internal/llm/tools/fetch.go Normal file
View File

@@ -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), &params); 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
}

View File

@@ -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,
)
}