1package chat
2
3import (
4 "encoding/json"
5 "strings"
6
7 tea "charm.land/bubbletea/v2"
8 "charm.land/lipgloss/v2"
9 "charm.land/lipgloss/v2/tree"
10 "github.com/charmbracelet/crush/internal/agent"
11 "github.com/charmbracelet/crush/internal/message"
12 "github.com/charmbracelet/crush/internal/ui/anim"
13 "github.com/charmbracelet/crush/internal/ui/styles"
14)
15
16// -----------------------------------------------------------------------------
17// Agent Tool
18// -----------------------------------------------------------------------------
19
20// NestedToolContainer is an interface for tool items that can contain nested tool calls.
21type NestedToolContainer interface {
22 NestedTools() []ToolMessageItem
23 SetNestedTools(tools []ToolMessageItem)
24 AddNestedTool(tool ToolMessageItem)
25}
26
27// AgentToolMessageItem is a message item that represents an agent tool call.
28type AgentToolMessageItem struct {
29 *baseToolMessageItem
30
31 nestedTools []ToolMessageItem
32}
33
34var (
35 _ ToolMessageItem = (*AgentToolMessageItem)(nil)
36 _ NestedToolContainer = (*AgentToolMessageItem)(nil)
37)
38
39// NewAgentToolMessageItem creates a new [AgentToolMessageItem].
40func NewAgentToolMessageItem(
41 sty *styles.Styles,
42 toolCall message.ToolCall,
43 result *message.ToolResult,
44 canceled bool,
45) *AgentToolMessageItem {
46 t := &AgentToolMessageItem{}
47 t.baseToolMessageItem = newBaseToolMessageItem(sty, toolCall, result, &AgentToolRenderContext{agent: t}, canceled)
48 // For the agent tool we keep spinning until the tool call is finished.
49 t.spinningFunc = func(state SpinningState) bool {
50 return !state.HasResult() && !state.IsCanceled()
51 }
52 return t
53}
54
55// Animate progresses the message animation if it should be spinning.
56func (a *AgentToolMessageItem) Animate(msg anim.StepMsg) tea.Cmd {
57 if a.result != nil || a.Status() == ToolStatusCanceled {
58 return nil
59 }
60 if msg.ID == a.ID() {
61 return a.anim.Animate(msg)
62 }
63 for _, nestedTool := range a.nestedTools {
64 if msg.ID != nestedTool.ID() {
65 continue
66 }
67 if s, ok := nestedTool.(Animatable); ok {
68 return s.Animate(msg)
69 }
70 }
71 return nil
72}
73
74// NestedTools returns the nested tools.
75func (a *AgentToolMessageItem) NestedTools() []ToolMessageItem {
76 return a.nestedTools
77}
78
79// SetNestedTools sets the nested tools.
80func (a *AgentToolMessageItem) SetNestedTools(tools []ToolMessageItem) {
81 a.nestedTools = tools
82 a.clearCache()
83}
84
85// AddNestedTool adds a nested tool.
86func (a *AgentToolMessageItem) AddNestedTool(tool ToolMessageItem) {
87 // Mark nested tools as simple (compact) rendering.
88 if s, ok := tool.(Compactable); ok {
89 s.SetCompact(true)
90 }
91 a.nestedTools = append(a.nestedTools, tool)
92 a.clearCache()
93}
94
95// AgentToolRenderContext renders agent tool messages.
96type AgentToolRenderContext struct {
97 agent *AgentToolMessageItem
98}
99
100// RenderTool implements the [ToolRenderer] interface.
101func (r *AgentToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
102 cappedWidth := cappedMessageWidth(width)
103 if !opts.ToolCall.Finished && !opts.IsCanceled() && len(r.agent.nestedTools) == 0 {
104 return pendingTool(sty, "Agent", opts.Anim)
105 }
106
107 var params agent.AgentParams
108 _ = json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms)
109
110 prompt := params.Prompt
111 prompt = strings.ReplaceAll(prompt, "\n", " ")
112
113 header := toolHeader(sty, opts.Status, "Agent", cappedWidth, opts.Compact)
114 if opts.Compact {
115 return header
116 }
117
118 // Build the task tag and prompt.
119 taskTag := sty.Tool.AgentTaskTag.Render("Task")
120 taskTagWidth := lipgloss.Width(taskTag)
121
122 // Calculate remaining width for prompt.
123 remainingWidth := min(cappedWidth-taskTagWidth-3, maxTextWidth-taskTagWidth-3) // -3 for spacing
124
125 promptText := sty.Tool.AgentPrompt.Width(remainingWidth).Render(prompt)
126
127 header = lipgloss.JoinVertical(
128 lipgloss.Left,
129 header,
130 "",
131 lipgloss.JoinHorizontal(
132 lipgloss.Left,
133 taskTag,
134 " ",
135 promptText,
136 ),
137 )
138
139 // Build tree with nested tool calls.
140 childTools := tree.Root(header)
141
142 for _, nestedTool := range r.agent.nestedTools {
143 childView := nestedTool.Render(remainingWidth)
144 childTools.Child(childView)
145 }
146
147 // Build parts.
148 var parts []string
149 parts = append(parts, childTools.Enumerator(roundedEnumerator(2, taskTagWidth-5)).String())
150
151 // Show animation if still running.
152 if !opts.HasResult() && !opts.IsCanceled() {
153 parts = append(parts, "", opts.Anim.Render())
154 }
155
156 result := lipgloss.JoinVertical(lipgloss.Left, parts...)
157
158 // Add body content when completed.
159 if opts.HasResult() && opts.Result.Content != "" {
160 body := toolOutputMarkdownContent(sty, opts.Result.Content, cappedWidth-toolBodyLeftPaddingTotal, opts.ExpandedContent)
161 return joinToolParts(result, body)
162 }
163
164 return result
165}
166
167// -----------------------------------------------------------------------------
168// Agentic Fetch Tool
169// -----------------------------------------------------------------------------
170
171// AgenticFetchToolMessageItem is a message item that represents an agentic fetch tool call.
172type AgenticFetchToolMessageItem struct {
173 *baseToolMessageItem
174
175 nestedTools []ToolMessageItem
176}
177
178var (
179 _ ToolMessageItem = (*AgenticFetchToolMessageItem)(nil)
180 _ NestedToolContainer = (*AgenticFetchToolMessageItem)(nil)
181)
182
183// NewAgenticFetchToolMessageItem creates a new [AgenticFetchToolMessageItem].
184func NewAgenticFetchToolMessageItem(
185 sty *styles.Styles,
186 toolCall message.ToolCall,
187 result *message.ToolResult,
188 canceled bool,
189) *AgenticFetchToolMessageItem {
190 t := &AgenticFetchToolMessageItem{}
191 t.baseToolMessageItem = newBaseToolMessageItem(sty, toolCall, result, &AgenticFetchToolRenderContext{fetch: t}, canceled)
192 // For the agentic fetch tool we keep spinning until the tool call is finished.
193 t.spinningFunc = func(state SpinningState) bool {
194 return !state.HasResult() && !state.IsCanceled()
195 }
196 return t
197}
198
199// NestedTools returns the nested tools.
200func (a *AgenticFetchToolMessageItem) NestedTools() []ToolMessageItem {
201 return a.nestedTools
202}
203
204// SetNestedTools sets the nested tools.
205func (a *AgenticFetchToolMessageItem) SetNestedTools(tools []ToolMessageItem) {
206 a.nestedTools = tools
207 a.clearCache()
208}
209
210// AddNestedTool adds a nested tool.
211func (a *AgenticFetchToolMessageItem) AddNestedTool(tool ToolMessageItem) {
212 // Mark nested tools as simple (compact) rendering.
213 if s, ok := tool.(Compactable); ok {
214 s.SetCompact(true)
215 }
216 a.nestedTools = append(a.nestedTools, tool)
217 a.clearCache()
218}
219
220// AgenticFetchToolRenderContext renders agentic fetch tool messages.
221type AgenticFetchToolRenderContext struct {
222 fetch *AgenticFetchToolMessageItem
223}
224
225// agenticFetchParams matches tools.AgenticFetchParams.
226type agenticFetchParams struct {
227 URL string `json:"url,omitempty"`
228 Prompt string `json:"prompt"`
229}
230
231// RenderTool implements the [ToolRenderer] interface.
232func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
233 cappedWidth := cappedMessageWidth(width)
234 if !opts.ToolCall.Finished && !opts.IsCanceled() && len(r.fetch.nestedTools) == 0 {
235 return pendingTool(sty, "Agentic Fetch", opts.Anim)
236 }
237
238 var params agenticFetchParams
239 _ = json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms)
240
241 prompt := params.Prompt
242 prompt = strings.ReplaceAll(prompt, "\n", " ")
243
244 // Build header with optional URL param.
245 toolParams := []string{}
246 if params.URL != "" {
247 toolParams = append(toolParams, params.URL)
248 }
249
250 header := toolHeader(sty, opts.Status, "Agentic Fetch", cappedWidth, opts.Compact, toolParams...)
251 if opts.Compact {
252 return header
253 }
254
255 // Build the prompt tag.
256 promptTag := sty.Tool.AgenticFetchPromptTag.Render("Prompt")
257 promptTagWidth := lipgloss.Width(promptTag)
258
259 // Calculate remaining width for prompt text.
260 remainingWidth := min(cappedWidth-promptTagWidth-3, maxTextWidth-promptTagWidth-3) // -3 for spacing
261
262 promptText := sty.Tool.AgentPrompt.Width(remainingWidth).Render(prompt)
263
264 header = lipgloss.JoinVertical(
265 lipgloss.Left,
266 header,
267 "",
268 lipgloss.JoinHorizontal(
269 lipgloss.Left,
270 promptTag,
271 " ",
272 promptText,
273 ),
274 )
275
276 // Build tree with nested tool calls.
277 childTools := tree.Root(header)
278
279 for _, nestedTool := range r.fetch.nestedTools {
280 childView := nestedTool.Render(remainingWidth)
281 childTools.Child(childView)
282 }
283
284 // Build parts.
285 var parts []string
286 parts = append(parts, childTools.Enumerator(roundedEnumerator(2, promptTagWidth-5)).String())
287
288 // Show animation if still running.
289 if !opts.HasResult() && !opts.IsCanceled() {
290 parts = append(parts, "", opts.Anim.Render())
291 }
292
293 result := lipgloss.JoinVertical(lipgloss.Left, parts...)
294
295 // Add body content when completed.
296 if opts.HasResult() && opts.Result.Content != "" {
297 body := toolOutputMarkdownContent(sty, opts.Result.Content, cappedWidth-toolBodyLeftPaddingTotal, opts.ExpandedContent)
298 return joinToolParts(result, body)
299 }
300
301 return result
302}