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.ToolCall.Finished && !opts.Canceled {
 42		return pendingTool(sty, "View", opts.Anim)
 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.Nested, toolParams...)
 60	if opts.Nested {
 61		return header
 62	}
 63
 64	if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
 65		return joinToolParts(header, earlyState)
 66	}
 67
 68	if opts.Result == nil {
 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	if content == "" {
 86		return header
 87	}
 88
 89	// Render code content with syntax highlighting.
 90	body := toolOutputCodeContent(sty, params.FilePath, content, params.Offset, cappedWidth, opts.Expanded)
 91	return joinToolParts(header, body)
 92}
 93
 94// -----------------------------------------------------------------------------
 95// Write Tool
 96// -----------------------------------------------------------------------------
 97
 98// WriteToolMessageItem is a message item that represents a write tool call.
 99type WriteToolMessageItem struct {
100	*baseToolMessageItem
101}
102
103var _ ToolMessageItem = (*WriteToolMessageItem)(nil)
104
105// NewWriteToolMessageItem creates a new [WriteToolMessageItem].
106func NewWriteToolMessageItem(
107	sty *styles.Styles,
108	toolCall message.ToolCall,
109	result *message.ToolResult,
110	canceled bool,
111) ToolMessageItem {
112	return newBaseToolMessageItem(sty, toolCall, result, &WriteToolRenderContext{}, canceled)
113}
114
115// WriteToolRenderContext renders write tool messages.
116type WriteToolRenderContext struct{}
117
118// RenderTool implements the [ToolRenderer] interface.
119func (w *WriteToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
120	cappedWidth := cappedMessageWidth(width)
121	if !opts.ToolCall.Finished && !opts.Canceled {
122		return pendingTool(sty, "Write", opts.Anim)
123	}
124
125	var params tools.WriteParams
126	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
127		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
128	}
129
130	file := fsext.PrettyPath(params.FilePath)
131	header := toolHeader(sty, opts.Status(), "Write", cappedWidth, opts.Nested, file)
132	if opts.Nested {
133		return header
134	}
135
136	if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
137		return joinToolParts(header, earlyState)
138	}
139
140	if params.Content == "" {
141		return header
142	}
143
144	// Render code content with syntax highlighting.
145	body := toolOutputCodeContent(sty, params.FilePath, params.Content, 0, cappedWidth, opts.Expanded)
146	return joinToolParts(header, body)
147}
148
149// -----------------------------------------------------------------------------
150// Edit Tool
151// -----------------------------------------------------------------------------
152
153// EditToolMessageItem is a message item that represents an edit tool call.
154type EditToolMessageItem struct {
155	*baseToolMessageItem
156}
157
158var _ ToolMessageItem = (*EditToolMessageItem)(nil)
159
160// NewEditToolMessageItem creates a new [EditToolMessageItem].
161func NewEditToolMessageItem(
162	sty *styles.Styles,
163	toolCall message.ToolCall,
164	result *message.ToolResult,
165	canceled bool,
166) ToolMessageItem {
167	return newBaseToolMessageItem(sty, toolCall, result, &EditToolRenderContext{}, canceled)
168}
169
170// EditToolRenderContext renders edit tool messages.
171type EditToolRenderContext struct{}
172
173// RenderTool implements the [ToolRenderer] interface.
174func (e *EditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
175	// Edit tool uses full width for diffs.
176	if !opts.ToolCall.Finished && !opts.Canceled {
177		return pendingTool(sty, "Edit", opts.Anim)
178	}
179
180	var params tools.EditParams
181	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
182		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width)
183	}
184
185	file := fsext.PrettyPath(params.FilePath)
186	header := toolHeader(sty, opts.Status(), "Edit", width, opts.Nested, file)
187	if opts.Nested {
188		return header
189	}
190
191	if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok {
192		return joinToolParts(header, earlyState)
193	}
194
195	if opts.Result == nil {
196		return header
197	}
198
199	// Get diff content from metadata.
200	var meta tools.EditResponseMetadata
201	if err := json.Unmarshal([]byte(opts.Result.Metadata), &meta); err != nil {
202		bodyWidth := width - toolBodyLeftPaddingTotal
203		body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.Expanded))
204		return joinToolParts(header, body)
205	}
206
207	// Render diff.
208	body := toolOutputDiffContent(sty, file, meta.OldContent, meta.NewContent, width, opts.Expanded)
209	return joinToolParts(header, body)
210}
211
212// -----------------------------------------------------------------------------
213// MultiEdit Tool
214// -----------------------------------------------------------------------------
215
216// MultiEditToolMessageItem is a message item that represents a multi-edit tool call.
217type MultiEditToolMessageItem struct {
218	*baseToolMessageItem
219}
220
221var _ ToolMessageItem = (*MultiEditToolMessageItem)(nil)
222
223// NewMultiEditToolMessageItem creates a new [MultiEditToolMessageItem].
224func NewMultiEditToolMessageItem(
225	sty *styles.Styles,
226	toolCall message.ToolCall,
227	result *message.ToolResult,
228	canceled bool,
229) ToolMessageItem {
230	return newBaseToolMessageItem(sty, toolCall, result, &MultiEditToolRenderContext{}, canceled)
231}
232
233// MultiEditToolRenderContext renders multi-edit tool messages.
234type MultiEditToolRenderContext struct{}
235
236// RenderTool implements the [ToolRenderer] interface.
237func (m *MultiEditToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
238	// MultiEdit tool uses full width for diffs.
239	if !opts.ToolCall.Finished && !opts.Canceled {
240		return pendingTool(sty, "Multi-Edit", opts.Anim)
241	}
242
243	var params tools.MultiEditParams
244	if err := json.Unmarshal([]byte(opts.ToolCall.Input), &params); err != nil {
245		return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width)
246	}
247
248	file := fsext.PrettyPath(params.FilePath)
249	toolParams := []string{file}
250	if len(params.Edits) > 0 {
251		toolParams = append(toolParams, "edits", fmt.Sprintf("%d", len(params.Edits)))
252	}
253
254	header := toolHeader(sty, opts.Status(), "Multi-Edit", width, opts.Nested, toolParams...)
255	if opts.Nested {
256		return header
257	}
258
259	if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok {
260		return joinToolParts(header, earlyState)
261	}
262
263	if opts.Result == nil {
264		return header
265	}
266
267	// Get diff content from metadata.
268	var meta tools.MultiEditResponseMetadata
269	if err := json.Unmarshal([]byte(opts.Result.Metadata), &meta); err != nil {
270		bodyWidth := width - toolBodyLeftPaddingTotal
271		body := sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.Expanded))
272		return joinToolParts(header, body)
273	}
274
275	// Render diff with optional failed edits note.
276	body := toolOutputMultiEditDiffContent(sty, file, meta, len(params.Edits), width, opts.Expanded)
277	return joinToolParts(header, body)
278}