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