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