file.go

  1package chat
  2
  3import (
  4	"encoding/json"
  5	"fmt"
  6	"strings"
  7
  8	"github.com/charmbracelet/crush/internal/agent/tools"
  9	"github.com/charmbracelet/crush/internal/fsext"
 10	"github.com/charmbracelet/crush/internal/message"
 11	"github.com/charmbracelet/crush/internal/ui/styles"
 12)
 13
 14// -----------------------------------------------------------------------------
 15// View Tool
 16// -----------------------------------------------------------------------------
 17
 18// ViewToolMessageItem is a message item that represents a view tool call.
 19type ViewToolMessageItem struct {
 20	*baseToolMessageItem
 21}
 22
 23var _ ToolMessageItem = (*ViewToolMessageItem)(nil)
 24
 25// NewViewToolMessageItem creates a new [ViewToolMessageItem].
 26func NewViewToolMessageItem(
 27	sty *styles.Styles,
 28	toolCall message.ToolCall,
 29	result *message.ToolResult,
 30	canceled bool,
 31) ToolMessageItem {
 32	return newBaseToolMessageItem(sty, toolCall, result, &ViewToolRenderContext{}, canceled)
 33}
 34
 35// ViewToolRenderContext renders view tool messages.
 36type ViewToolRenderContext struct{}
 37
 38// RenderTool implements the [ToolRenderer] interface.
 39func (v *ViewToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
 40	if opts.IsPending() {
 41		return pendingTool(sty, "View", opts.Anim)
 42	}
 43
 44	var params tools.ViewParams
 45	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
 46		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width)
 47	}
 48
 49	file := fsext.PrettyPath(params.FilePath)
 50	toolParams := []string{file}
 51	if params.Limit != 0 {
 52		toolParams = append(toolParams, "limit", fmt.Sprintf("%d", params.Limit))
 53	}
 54	if params.Offset != 0 {
 55		toolParams = append(toolParams, "offset", fmt.Sprintf("%d", params.Offset))
 56	}
 57
 58	header := toolHeader(sty, opts.Status, "View", width, opts.Compact, toolParams...)
 59	if opts.Compact {
 60		return header
 61	}
 62
 63	if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok {
 64		return joinToolParts(header, earlyState)
 65	}
 66
 67	if !opts.HasResult() {
 68		return header
 69	}
 70
 71	// Handle image content.
 72	if opts.Result.Data != "" && strings.HasPrefix(opts.Result.MIMEType, "image/") {
 73		body := toolOutputImageContent(sty, opts.Result.Data, opts.Result.MIMEType)
 74		return joinToolParts(header, body)
 75	}
 76
 77	// Try to get content from metadata first (contains actual file content).
 78	var meta tools.ViewResponseMetadata
 79	content := opts.Result.Content
 80	if err := json.Unmarshal([]byte(opts.Result.Metadata), &meta); err == nil && meta.Content != "" {
 81		content = meta.Content
 82	}
 83
 84	if content == "" {
 85		return header
 86	}
 87
 88	// Render code content with syntax highlighting.
 89	body := toolOutputCodeContent(sty, params.FilePath, content, params.Offset, width, opts.ExpandedContent)
 90	return joinToolParts(header, body)
 91}
 92
 93// -----------------------------------------------------------------------------
 94// Write Tool
 95// -----------------------------------------------------------------------------
 96
 97// WriteToolMessageItem is a message item that represents a write tool call.
 98type WriteToolMessageItem struct {
 99	*baseToolMessageItem
100}
101
102var _ ToolMessageItem = (*WriteToolMessageItem)(nil)
103
104// NewWriteToolMessageItem creates a new [WriteToolMessageItem].
105func NewWriteToolMessageItem(
106	sty *styles.Styles,
107	toolCall message.ToolCall,
108	result *message.ToolResult,
109	canceled bool,
110) ToolMessageItem {
111	return newBaseToolMessageItem(sty, toolCall, result, &WriteToolRenderContext{}, canceled)
112}
113
114// WriteToolRenderContext renders write tool messages.
115type WriteToolRenderContext struct{}
116
117// RenderTool implements the [ToolRenderer] interface.
118func (w *WriteToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
119	if opts.IsPending() {
120		return pendingTool(sty, "Write", opts.Anim)
121	}
122
123	var params tools.WriteParams
124	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
125		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width)
126	}
127
128	file := fsext.PrettyPath(params.FilePath)
129	header := toolHeader(sty, opts.Status, "Write", width, opts.Compact, file)
130	if opts.Compact {
131		return header
132	}
133
134	if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok {
135		return joinToolParts(header, earlyState)
136	}
137
138	if params.Content == "" {
139		return header
140	}
141
142	// Render code content with syntax highlighting.
143	body := toolOutputCodeContent(sty, params.FilePath, params.Content, 0, width, opts.ExpandedContent)
144	return joinToolParts(header, body)
145}
146
147// -----------------------------------------------------------------------------
148// Edit Tool
149// -----------------------------------------------------------------------------
150
151// EditToolMessageItem is a message item that represents an edit tool call.
152type EditToolMessageItem struct {
153	*baseToolMessageItem
154}
155
156var _ ToolMessageItem = (*EditToolMessageItem)(nil)
157
158// NewEditToolMessageItem creates a new [EditToolMessageItem].
159func NewEditToolMessageItem(
160	sty *styles.Styles,
161	toolCall message.ToolCall,
162	result *message.ToolResult,
163	canceled bool,
164) ToolMessageItem {
165	return newBaseToolMessageItem(sty, toolCall, result, &EditToolRenderContext{}, canceled)
166}
167
168// EditToolRenderContext renders edit tool messages.
169type EditToolRenderContext struct{}
170
171// RenderTool implements the [ToolRenderer] interface.
172func (e *EditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
173	// Edit tool uses full width for diffs.
174	if opts.IsPending() {
175		return pendingTool(sty, "Edit", opts.Anim)
176	}
177
178	var params tools.EditParams
179	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
180		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width)
181	}
182
183	file := fsext.PrettyPath(params.FilePath)
184	header := toolHeader(sty, opts.Status, "Edit", width, opts.Compact, file)
185	if opts.Compact {
186		return header
187	}
188
189	if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok {
190		return joinToolParts(header, earlyState)
191	}
192
193	if !opts.HasResult() {
194		return header
195	}
196
197	// Get diff content from metadata.
198	var meta tools.EditResponseMetadata
199	if err := json.Unmarshal([]byte(opts.Result.Metadata), &meta); err != nil {
200		bodyWidth := width - toolBodyLeftPaddingTotal
201		body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
202		return joinToolParts(header, body)
203	}
204
205	// Render diff.
206	body := toolOutputDiffContent(sty, file, meta.OldContent, meta.NewContent, width, opts.ExpandedContent)
207	return joinToolParts(header, body)
208}
209
210// -----------------------------------------------------------------------------
211// MultiEdit Tool
212// -----------------------------------------------------------------------------
213
214// MultiEditToolMessageItem is a message item that represents a multi-edit tool call.
215type MultiEditToolMessageItem struct {
216	*baseToolMessageItem
217}
218
219var _ ToolMessageItem = (*MultiEditToolMessageItem)(nil)
220
221// NewMultiEditToolMessageItem creates a new [MultiEditToolMessageItem].
222func NewMultiEditToolMessageItem(
223	sty *styles.Styles,
224	toolCall message.ToolCall,
225	result *message.ToolResult,
226	canceled bool,
227) ToolMessageItem {
228	return newBaseToolMessageItem(sty, toolCall, result, &MultiEditToolRenderContext{}, canceled)
229}
230
231// MultiEditToolRenderContext renders multi-edit tool messages.
232type MultiEditToolRenderContext struct{}
233
234// RenderTool implements the [ToolRenderer] interface.
235func (m *MultiEditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
236	// MultiEdit tool uses full width for diffs.
237	if opts.IsPending() {
238		return pendingTool(sty, "Multi-Edit", opts.Anim)
239	}
240
241	var params tools.MultiEditParams
242	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
243		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width)
244	}
245
246	file := fsext.PrettyPath(params.FilePath)
247	toolParams := []string{file}
248	if len(params.Edits) > 0 {
249		toolParams = append(toolParams, "edits", fmt.Sprintf("%d", len(params.Edits)))
250	}
251
252	header := toolHeader(sty, opts.Status, "Multi-Edit", width, opts.Compact, toolParams...)
253	if opts.Compact {
254		return header
255	}
256
257	if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok {
258		return joinToolParts(header, earlyState)
259	}
260
261	if !opts.HasResult() {
262		return header
263	}
264
265	// Get diff content from metadata.
266	var meta tools.MultiEditResponseMetadata
267	if err := json.Unmarshal([]byte(opts.Result.Metadata), &meta); err != nil {
268		bodyWidth := width - toolBodyLeftPaddingTotal
269		body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
270		return joinToolParts(header, body)
271	}
272
273	// Render diff with optional failed edits note.
274	body := toolOutputMultiEditDiffContent(sty, file, meta, len(params.Edits), width, opts.ExpandedContent)
275	return joinToolParts(header, body)
276}
277
278// -----------------------------------------------------------------------------
279// Download Tool
280// -----------------------------------------------------------------------------
281
282// DownloadToolMessageItem is a message item that represents a download tool call.
283type DownloadToolMessageItem struct {
284	*baseToolMessageItem
285}
286
287var _ ToolMessageItem = (*DownloadToolMessageItem)(nil)
288
289// NewDownloadToolMessageItem creates a new [DownloadToolMessageItem].
290func NewDownloadToolMessageItem(
291	sty *styles.Styles,
292	toolCall message.ToolCall,
293	result *message.ToolResult,
294	canceled bool,
295) ToolMessageItem {
296	return newBaseToolMessageItem(sty, toolCall, result, &DownloadToolRenderContext{}, canceled)
297}
298
299// DownloadToolRenderContext renders download tool messages.
300type DownloadToolRenderContext struct{}
301
302// RenderTool implements the [ToolRenderer] interface.
303func (d *DownloadToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
304	if opts.IsPending() {
305		return pendingTool(sty, "Download", opts.Anim)
306	}
307
308	var params tools.DownloadParams
309	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
310		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width)
311	}
312
313	toolParams := []string{params.URL}
314	if params.FilePath != "" {
315		toolParams = append(toolParams, "file_path", fsext.PrettyPath(params.FilePath))
316	}
317	if params.Timeout != 0 {
318		toolParams = append(toolParams, "timeout", formatTimeout(params.Timeout))
319	}
320
321	header := toolHeader(sty, opts.Status, "Download", width, opts.Compact, toolParams...)
322	if opts.Compact {
323		return header
324	}
325
326	if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok {
327		return joinToolParts(header, earlyState)
328	}
329
330	if opts.HasEmptyResult() {
331		return header
332	}
333
334	bodyWidth := width - toolBodyLeftPaddingTotal
335	body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
336	return joinToolParts(header, body)
337}