fetch.go

  1package chat
  2
  3import (
  4	"encoding/json"
  5
  6	"github.com/charmbracelet/crush/internal/agent/tools"
  7	"github.com/charmbracelet/crush/internal/message"
  8	"github.com/charmbracelet/crush/internal/ui/styles"
  9)
 10
 11// -----------------------------------------------------------------------------
 12// Fetch Tool
 13// -----------------------------------------------------------------------------
 14
 15// FetchToolMessageItem is a message item that represents a fetch tool call.
 16type FetchToolMessageItem struct {
 17	*baseToolMessageItem
 18}
 19
 20var _ ToolMessageItem = (*FetchToolMessageItem)(nil)
 21
 22// NewFetchToolMessageItem creates a new [FetchToolMessageItem].
 23func NewFetchToolMessageItem(
 24	sty *styles.Styles,
 25	toolCall message.ToolCall,
 26	result *message.ToolResult,
 27	canceled bool,
 28) ToolMessageItem {
 29	return newBaseToolMessageItem(sty, toolCall, result, &FetchToolRenderContext{}, canceled)
 30}
 31
 32// FetchToolRenderContext renders fetch tool messages.
 33type FetchToolRenderContext struct{}
 34
 35// RenderTool implements the [ToolRenderer] interface.
 36func (f *FetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 37	cappedWidth := cappedMessageWidth(width)
 38	if opts.IsPending() {
 39		return pendingTool(sty, "Fetch", opts.Anim)
 40	}
 41
 42	var params tools.FetchParams
 43	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
 44		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
 45	}
 46
 47	toolParams := []string{params.URL}
 48	if params.Format != "" {
 49		toolParams = append(toolParams, "format", params.Format)
 50	}
 51	if params.Timeout != 0 {
 52		toolParams = append(toolParams, "timeout", formatTimeout(params.Timeout))
 53	}
 54
 55	header := toolHeader(sty, opts.Status, "Fetch", cappedWidth, opts.Compact, toolParams...)
 56	if opts.Compact {
 57		return header
 58	}
 59
 60	if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
 61		return joinToolParts(header, earlyState)
 62	}
 63
 64	if opts.HasEmptyResult() {
 65		return header
 66	}
 67
 68	// Determine file extension for syntax highlighting based on format.
 69	file := getFileExtensionForFormat(params.Format)
 70	body := toolOutputCodeContent(sty, file, opts.Result.Content, 0, cappedWidth, opts.ExpandedContent)
 71	return joinToolParts(header, body)
 72}
 73
 74// getFileExtensionForFormat returns a filename with appropriate extension for syntax highlighting.
 75func getFileExtensionForFormat(format string) string {
 76	switch format {
 77	case "text":
 78		return "fetch.txt"
 79	case "html":
 80		return "fetch.html"
 81	default:
 82		return "fetch.md"
 83	}
 84}
 85
 86// -----------------------------------------------------------------------------
 87// WebFetch Tool
 88// -----------------------------------------------------------------------------
 89
 90// WebFetchToolMessageItem is a message item that represents a web_fetch tool call.
 91type WebFetchToolMessageItem struct {
 92	*baseToolMessageItem
 93}
 94
 95var _ ToolMessageItem = (*WebFetchToolMessageItem)(nil)
 96
 97// NewWebFetchToolMessageItem creates a new [WebFetchToolMessageItem].
 98func NewWebFetchToolMessageItem(
 99	sty *styles.Styles,
100	toolCall message.ToolCall,
101	result *message.ToolResult,
102	canceled bool,
103) ToolMessageItem {
104	return newBaseToolMessageItem(sty, toolCall, result, &WebFetchToolRenderContext{}, canceled)
105}
106
107// WebFetchToolRenderContext renders web_fetch tool messages.
108type WebFetchToolRenderContext struct{}
109
110// RenderTool implements the [ToolRenderer] interface.
111func (w *WebFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
112	cappedWidth := cappedMessageWidth(width)
113	if opts.IsPending() {
114		return pendingTool(sty, "Fetch", opts.Anim)
115	}
116
117	var params tools.WebFetchParams
118	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
119		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
120	}
121
122	toolParams := []string{params.URL}
123	header := toolHeader(sty, opts.Status, "Fetch", cappedWidth, opts.Compact, toolParams...)
124	if opts.Compact {
125		return header
126	}
127
128	if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
129		return joinToolParts(header, earlyState)
130	}
131
132	if opts.HasEmptyResult() {
133		return header
134	}
135
136	body := toolOutputMarkdownContent(sty, opts.Result.Content, cappedWidth, opts.ExpandedContent)
137	return joinToolParts(header, body)
138}
139
140// -----------------------------------------------------------------------------
141// WebSearch Tool
142// -----------------------------------------------------------------------------
143
144// WebSearchToolMessageItem is a message item that represents a web_search tool call.
145type WebSearchToolMessageItem struct {
146	*baseToolMessageItem
147}
148
149var _ ToolMessageItem = (*WebSearchToolMessageItem)(nil)
150
151// NewWebSearchToolMessageItem creates a new [WebSearchToolMessageItem].
152func NewWebSearchToolMessageItem(
153	sty *styles.Styles,
154	toolCall message.ToolCall,
155	result *message.ToolResult,
156	canceled bool,
157) ToolMessageItem {
158	return newBaseToolMessageItem(sty, toolCall, result, &WebSearchToolRenderContext{}, canceled)
159}
160
161// WebSearchToolRenderContext renders web_search tool messages.
162type WebSearchToolRenderContext struct{}
163
164// RenderTool implements the [ToolRenderer] interface.
165func (w *WebSearchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
166	cappedWidth := cappedMessageWidth(width)
167	if opts.IsPending() {
168		return pendingTool(sty, "Search", opts.Anim)
169	}
170
171	var params tools.WebSearchParams
172	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
173		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
174	}
175
176	toolParams := []string{params.Query}
177	header := toolHeader(sty, opts.Status, "Search", cappedWidth, opts.Compact, toolParams...)
178	if opts.Compact {
179		return header
180	}
181
182	if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
183		return joinToolParts(header, earlyState)
184	}
185
186	if opts.HasEmptyResult() {
187		return header
188	}
189
190	body := toolOutputMarkdownContent(sty, opts.Result.Content, cappedWidth, opts.ExpandedContent)
191	return joinToolParts(header, body)
192}