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