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