summaryrefslogtreecommitdiffhomepage
path: root/internal
diff options
context:
space:
mode:
authorKujtim Hoxha <[email protected]>2025-04-03 19:50:31 +0200
committerKujtim Hoxha <[email protected]>2025-04-03 19:50:31 +0200
commit49b593d9ddbe278358b30dcde444932172bea615 (patch)
treedbe6c779ca4932ff369f3684dbefd3e0e599e0fd /internal
parent230917bbbf5fa2cbdb37f298554889a7a5233744 (diff)
downloadopencode-49b593d9ddbe278358b30dcde444932172bea615.tar.gz
opencode-49b593d9ddbe278358b30dcde444932172bea615.zip
small fixes
Diffstat (limited to 'internal')
-rw-r--r--internal/llm/agent/coder.go2
-rw-r--r--internal/llm/tools/bash.go4
-rw-r--r--internal/llm/tools/edit.go7
-rw-r--r--internal/llm/tools/fetch.go223
-rw-r--r--internal/tui/components/dialog/permission.go17
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), &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
+}
+
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,
)
}