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), ¶ms); 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), ¶ms); 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), ¶ms); 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), ¶ms); 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), ¶ms); 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}