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// Touch Tool
102// -----------------------------------------------------------------------------
103
104// TouchToolMessageItem is a message item that represents a touch tool call.
105type TouchToolMessageItem struct {
106	*baseToolMessageItem
107}
108
109var _ ToolMessageItem = (*TouchToolMessageItem)(nil)
110
111// NewTouchToolMessageItem creates a new [TouchToolMessageItem].
112func NewTouchToolMessageItem(
113	sty *styles.Styles,
114	toolCall message.ToolCall,
115	result *message.ToolResult,
116	canceled bool,
117) ToolMessageItem {
118	return newBaseToolMessageItem(sty, toolCall, result, &TouchToolRenderContext{}, canceled)
119}
120
121// TouchToolRenderContext renders touch tool messages.
122type TouchToolRenderContext struct{}
123
124// RenderTool implements the [ToolRenderer] interface.
125func (t *TouchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
126	cappedWidth := cappedMessageWidth(width)
127	if opts.IsPending() {
128		return pendingTool(sty, "Touch", opts.Anim, opts.Compact)
129	}
130
131	var params tools.TouchParams
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, "Touch", cappedWidth, opts.Compact, file)
138	if opts.Compact {
139		return header
140	}
141
142	if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
143		return joinToolParts(header, earlyState)
144	}
145
146	return header
147}
148
149// -----------------------------------------------------------------------------
150// Write Tool
151// -----------------------------------------------------------------------------
152
153// WriteToolMessageItem is a message item that represents a write tool call.
154type WriteToolMessageItem struct {
155	*baseToolMessageItem
156}
157
158var _ ToolMessageItem = (*WriteToolMessageItem)(nil)
159
160// NewWriteToolMessageItem creates a new [WriteToolMessageItem].
161func NewWriteToolMessageItem(
162	sty *styles.Styles,
163	toolCall message.ToolCall,
164	result *message.ToolResult,
165	canceled bool,
166) ToolMessageItem {
167	return newBaseToolMessageItem(sty, toolCall, result, &WriteToolRenderContext{}, canceled)
168}
169
170// WriteToolRenderContext renders write tool messages.
171type WriteToolRenderContext struct{}
172
173// RenderTool implements the [ToolRenderer] interface.
174func (w *WriteToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
175	cappedWidth := cappedMessageWidth(width)
176	if opts.IsPending() {
177		return pendingTool(sty, "Write", opts.Anim, opts.Compact)
178	}
179
180	var params tools.WriteParams
181	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
182		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
183	}
184
185	file := fsext.PrettyPath(params.FilePath)
186	header := toolHeader(sty, opts.Status, "Write", cappedWidth, opts.Compact, file)
187	if opts.Compact {
188		return header
189	}
190
191	if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
192		return joinToolParts(header, earlyState)
193	}
194
195	if params.Content == "" {
196		return header
197	}
198
199	// Render code content with syntax highlighting.
200	body := toolOutputCodeContent(sty, params.FilePath, params.Content, 0, cappedWidth, opts.ExpandedContent)
201	return joinToolParts(header, body)
202}
203
204// -----------------------------------------------------------------------------
205// Edit Tool
206// -----------------------------------------------------------------------------
207
208// EditToolMessageItem is a message item that represents an edit tool call.
209type EditToolMessageItem struct {
210	*baseToolMessageItem
211}
212
213var _ ToolMessageItem = (*EditToolMessageItem)(nil)
214
215// NewEditToolMessageItem creates a new [EditToolMessageItem].
216func NewEditToolMessageItem(
217	sty *styles.Styles,
218	toolCall message.ToolCall,
219	result *message.ToolResult,
220	canceled bool,
221) ToolMessageItem {
222	return newBaseToolMessageItem(sty, toolCall, result, &EditToolRenderContext{}, canceled)
223}
224
225// EditToolRenderContext renders edit tool messages.
226type EditToolRenderContext struct{}
227
228// RenderTool implements the [ToolRenderer] interface.
229func (e *EditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
230	// Edit tool uses full width for diffs.
231	if opts.IsPending() {
232		return pendingTool(sty, "Edit", opts.Anim, opts.Compact)
233	}
234
235	var params tools.EditParams
236	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
237		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width)
238	}
239
240	file := fsext.PrettyPath(params.FilePath)
241	header := toolHeader(sty, opts.Status, "Edit", width, opts.Compact, file)
242	if opts.Compact {
243		return header
244	}
245
246	if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok {
247		return joinToolParts(header, earlyState)
248	}
249
250	if !opts.HasResult() {
251		return header
252	}
253
254	// Get diff content from metadata.
255	var meta tools.EditResponseMetadata
256	if err := json.Unmarshal([]byte(opts.Result.Metadata), &meta); err != nil {
257		bodyWidth := width - toolBodyLeftPaddingTotal
258		body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
259		return joinToolParts(header, body)
260	}
261
262	// Render diff.
263	body := toolOutputDiffContent(sty, file, meta.OldContent, meta.NewContent, width, opts.ExpandedContent)
264	return joinToolParts(header, body)
265}
266
267// -----------------------------------------------------------------------------
268// MultiEdit Tool
269// -----------------------------------------------------------------------------
270
271// MultiEditToolMessageItem is a message item that represents a multi-edit tool call.
272type MultiEditToolMessageItem struct {
273	*baseToolMessageItem
274}
275
276var _ ToolMessageItem = (*MultiEditToolMessageItem)(nil)
277
278// NewMultiEditToolMessageItem creates a new [MultiEditToolMessageItem].
279func NewMultiEditToolMessageItem(
280	sty *styles.Styles,
281	toolCall message.ToolCall,
282	result *message.ToolResult,
283	canceled bool,
284) ToolMessageItem {
285	return newBaseToolMessageItem(sty, toolCall, result, &MultiEditToolRenderContext{}, canceled)
286}
287
288// MultiEditToolRenderContext renders multi-edit tool messages.
289type MultiEditToolRenderContext struct{}
290
291// RenderTool implements the [ToolRenderer] interface.
292func (m *MultiEditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
293	// MultiEdit tool uses full width for diffs.
294	if opts.IsPending() {
295		return pendingTool(sty, "Multi-Edit", opts.Anim, opts.Compact)
296	}
297
298	var params tools.MultiEditParams
299	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
300		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width)
301	}
302
303	file := fsext.PrettyPath(params.FilePath)
304	toolParams := []string{file}
305	if len(params.Edits) > 0 {
306		toolParams = append(toolParams, "edits", fmt.Sprintf("%d", len(params.Edits)))
307	}
308
309	header := toolHeader(sty, opts.Status, "Multi-Edit", width, opts.Compact, toolParams...)
310	if opts.Compact {
311		return header
312	}
313
314	if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok {
315		return joinToolParts(header, earlyState)
316	}
317
318	if !opts.HasResult() {
319		return header
320	}
321
322	// Get diff content from metadata.
323	var meta tools.MultiEditResponseMetadata
324	if err := json.Unmarshal([]byte(opts.Result.Metadata), &meta); err != nil {
325		bodyWidth := width - toolBodyLeftPaddingTotal
326		body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
327		return joinToolParts(header, body)
328	}
329
330	// Render diff with optional failed edits note.
331	body := toolOutputMultiEditDiffContent(sty, file, meta, len(params.Edits), width, opts.ExpandedContent)
332	return joinToolParts(header, body)
333}
334
335// -----------------------------------------------------------------------------
336// Download Tool
337// -----------------------------------------------------------------------------
338
339// DownloadToolMessageItem is a message item that represents a download tool call.
340type DownloadToolMessageItem struct {
341	*baseToolMessageItem
342}
343
344var _ ToolMessageItem = (*DownloadToolMessageItem)(nil)
345
346// NewDownloadToolMessageItem creates a new [DownloadToolMessageItem].
347func NewDownloadToolMessageItem(
348	sty *styles.Styles,
349	toolCall message.ToolCall,
350	result *message.ToolResult,
351	canceled bool,
352) ToolMessageItem {
353	return newBaseToolMessageItem(sty, toolCall, result, &DownloadToolRenderContext{}, canceled)
354}
355
356// DownloadToolRenderContext renders download tool messages.
357type DownloadToolRenderContext struct{}
358
359// RenderTool implements the [ToolRenderer] interface.
360func (d *DownloadToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
361	cappedWidth := cappedMessageWidth(width)
362	if opts.IsPending() {
363		return pendingTool(sty, "Download", opts.Anim, opts.Compact)
364	}
365
366	var params tools.DownloadParams
367	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
368		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
369	}
370
371	toolParams := []string{params.URL}
372	if params.FilePath != "" {
373		toolParams = append(toolParams, "file_path", fsext.PrettyPath(params.FilePath))
374	}
375	if params.Timeout != 0 {
376		toolParams = append(toolParams, "timeout", formatTimeout(params.Timeout))
377	}
378
379	header := toolHeader(sty, opts.Status, "Download", cappedWidth, opts.Compact, toolParams...)
380	if opts.Compact {
381		return header
382	}
383
384	if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
385		return joinToolParts(header, earlyState)
386	}
387
388	if opts.HasEmptyResult() {
389		return header
390	}
391
392	bodyWidth := cappedWidth - toolBodyLeftPaddingTotal
393	body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
394	return joinToolParts(header, body)
395}