@@ -0,0 +1,278 @@
1+package chat
2+
3+import (
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.
19+type ViewToolMessageItem struct {
20+ *baseToolMessageItem
21+}
22+
23+var _ ToolMessageItem = (*ViewToolMessageItem)(nil)
24+
25+// NewViewToolMessageItem creates a new [ViewToolMessageItem].
26+func 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.
36+type ViewToolRenderContext struct{}
37+
38+// RenderTool implements the [ToolRenderer] interface.
39+func (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.
99+type WriteToolMessageItem struct {
100+ *baseToolMessageItem
101+}
102+
103+var _ ToolMessageItem = (*WriteToolMessageItem)(nil)
104+
105+// NewWriteToolMessageItem creates a new [WriteToolMessageItem].
106+func 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.
116+type WriteToolRenderContext struct{}
117+
118+// RenderTool implements the [ToolRenderer] interface.
119+func (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.
154+type EditToolMessageItem struct {
155+ *baseToolMessageItem
156+}
157+
158+var _ ToolMessageItem = (*EditToolMessageItem)(nil)
159+
160+// NewEditToolMessageItem creates a new [EditToolMessageItem].
161+func 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.
171+type EditToolRenderContext struct{}
172+
173+// RenderTool implements the [ToolRenderer] interface.
174+func (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.
217+type MultiEditToolMessageItem struct {
218+ *baseToolMessageItem
219+}
220+
221+var _ ToolMessageItem = (*MultiEditToolMessageItem)(nil)
222+
223+// NewMultiEditToolMessageItem creates a new [MultiEditToolMessageItem].
224+func 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.
234+type MultiEditToolRenderContext struct{}
235+
236+// RenderTool implements the [ToolRenderer] interface.
237+func (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+}