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 if !opts.ToolCall.Finished && !opts.IsCanceled() && len(r.agent.nestedTools) == 0 {
103 return pendingTool(sty, "Agent", opts.Anim)
104 }
105
106 var params agent.AgentParams
107 _ = json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms)
108
109 prompt := params.Prompt
110 prompt = strings.ReplaceAll(prompt, "\n", " ")
111
112 header := toolHeader(sty, opts.Status, "Agent", width, opts.Compact)
113 if opts.Compact {
114 return header
115 }
116
117 // Build the task tag and prompt.
118 taskTag := sty.Tool.AgentTaskTag.Render("Task")
119 taskTagWidth := lipgloss.Width(taskTag)
120
121 // Calculate remaining width for prompt.
122 remainingWidth := width - taskTagWidth - 3 // -3 for spacing
123
124 promptText := sty.Tool.AgentPrompt.Width(remainingWidth).Render(prompt)
125
126 header = lipgloss.JoinVertical(
127 lipgloss.Left,
128 header,
129 "",
130 lipgloss.JoinHorizontal(
131 lipgloss.Left,
132 taskTag,
133 " ",
134 promptText,
135 ),
136 )
137
138 // Build tree with nested tool calls.
139 childTools := tree.Root(header)
140
141 for _, nestedTool := range r.agent.nestedTools {
142 childView := nestedTool.Render(remainingWidth)
143 childTools.Child(childView)
144 }
145
146 // Build parts.
147 var parts []string
148 parts = append(parts, childTools.Enumerator(roundedEnumerator(2, taskTagWidth-5)).String())
149
150 // Show animation if still running.
151 if !opts.HasResult() && !opts.IsCanceled() {
152 parts = append(parts, "", opts.Anim.Render())
153 }
154
155 result := lipgloss.JoinVertical(lipgloss.Left, parts...)
156
157 // Add body content when completed.
158 if opts.HasResult() && opts.Result.Content != "" {
159 body := toolOutputMarkdownContent(sty, opts.Result.Content, width-toolBodyLeftPaddingTotal, opts.ExpandedContent)
160 return joinToolParts(result, body)
161 }
162
163 return result
164}
165
166// -----------------------------------------------------------------------------
167// Agentic Fetch Tool
168// -----------------------------------------------------------------------------
169
170// AgenticFetchToolMessageItem is a message item that represents an agentic fetch tool call.
171type AgenticFetchToolMessageItem struct {
172 *baseToolMessageItem
173
174 nestedTools []ToolMessageItem
175}
176
177var (
178 _ ToolMessageItem = (*AgenticFetchToolMessageItem)(nil)
179 _ NestedToolContainer = (*AgenticFetchToolMessageItem)(nil)
180)
181
182// NewAgenticFetchToolMessageItem creates a new [AgenticFetchToolMessageItem].
183func NewAgenticFetchToolMessageItem(
184 sty *styles.Styles,
185 toolCall message.ToolCall,
186 result *message.ToolResult,
187 canceled bool,
188) *AgenticFetchToolMessageItem {
189 t := &AgenticFetchToolMessageItem{}
190 t.baseToolMessageItem = newBaseToolMessageItem(sty, toolCall, result, &AgenticFetchToolRenderContext{fetch: t}, canceled)
191 // For the agentic fetch tool we keep spinning until the tool call is finished.
192 t.spinningFunc = func(state SpinningState) bool {
193 return !state.HasResult() && !state.IsCanceled()
194 }
195 return t
196}
197
198// NestedTools returns the nested tools.
199func (a *AgenticFetchToolMessageItem) NestedTools() []ToolMessageItem {
200 return a.nestedTools
201}
202
203// SetNestedTools sets the nested tools.
204func (a *AgenticFetchToolMessageItem) SetNestedTools(tools []ToolMessageItem) {
205 a.nestedTools = tools
206 a.clearCache()
207}
208
209// AddNestedTool adds a nested tool.
210func (a *AgenticFetchToolMessageItem) AddNestedTool(tool ToolMessageItem) {
211 // Mark nested tools as simple (compact) rendering.
212 if s, ok := tool.(Compactable); ok {
213 s.SetCompact(true)
214 }
215 a.nestedTools = append(a.nestedTools, tool)
216 a.clearCache()
217}
218
219// AgenticFetchToolRenderContext renders agentic fetch tool messages.
220type AgenticFetchToolRenderContext struct {
221 fetch *AgenticFetchToolMessageItem
222}
223
224// agenticFetchParams matches tools.AgenticFetchParams.
225type agenticFetchParams struct {
226 URL string `json:"url,omitempty"`
227 Prompt string `json:"prompt"`
228}
229
230// RenderTool implements the [ToolRenderer] interface.
231func (r *AgenticFetchToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
232 if !opts.ToolCall.Finished && !opts.IsCanceled() && len(r.fetch.nestedTools) == 0 {
233 return pendingTool(sty, "Agentic Fetch", opts.Anim)
234 }
235
236 var params agenticFetchParams
237 _ = json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms)
238
239 prompt := params.Prompt
240 prompt = strings.ReplaceAll(prompt, "\n", " ")
241
242 // Build header with optional URL param.
243 toolParams := []string{}
244 if params.URL != "" {
245 toolParams = append(toolParams, params.URL)
246 }
247
248 header := toolHeader(sty, opts.Status, "Agentic Fetch", width, opts.Compact, toolParams...)
249 if opts.Compact {
250 return header
251 }
252
253 // Build the prompt tag.
254 promptTag := sty.Tool.AgenticFetchPromptTag.Render("Prompt")
255 promptTagWidth := lipgloss.Width(promptTag)
256
257 // Calculate remaining width for prompt text.
258 remainingWidth := width - promptTagWidth - 3 // -3 for spacing
259
260 promptText := sty.Tool.AgentPrompt.Width(remainingWidth).Render(prompt)
261
262 header = lipgloss.JoinVertical(
263 lipgloss.Left,
264 header,
265 "",
266 lipgloss.JoinHorizontal(
267 lipgloss.Left,
268 promptTag,
269 " ",
270 promptText,
271 ),
272 )
273
274 // Build tree with nested tool calls.
275 childTools := tree.Root(header)
276
277 for _, nestedTool := range r.fetch.nestedTools {
278 childView := nestedTool.Render(remainingWidth)
279 childTools.Child(childView)
280 }
281
282 // Build parts.
283 var parts []string
284 parts = append(parts, childTools.Enumerator(roundedEnumerator(2, promptTagWidth-5)).String())
285
286 // Show animation if still running.
287 if !opts.HasResult() && !opts.IsCanceled() {
288 parts = append(parts, "", opts.Anim.Render())
289 }
290
291 result := lipgloss.JoinVertical(lipgloss.Left, parts...)
292
293 // Add body content when completed.
294 if opts.HasResult() && opts.Result.Content != "" {
295 body := toolOutputMarkdownContent(sty, opts.Result.Content, width-toolBodyLeftPaddingTotal, opts.ExpandedContent)
296 return joinToolParts(result, body)
297 }
298
299 return result
300}