@@ -0,0 +1,329 @@
7+ tea "charm.land/bubbletea/v2"
8+ "charm.land/lipgloss/v2"
9+ "github.com/charmbracelet/crush/internal/message"
10+ "github.com/charmbracelet/crush/internal/ui/anim"
11+ "github.com/charmbracelet/crush/internal/ui/styles"
12+ "github.com/charmbracelet/x/ansi"
15+// responseContextHeight limits the number of lines displayed in tool output.
16+const responseContextHeight = 10
18+// toolBodyLeftPaddingTotal represents the padding that should be applied to each tool body
19+const toolBodyLeftPaddingTotal = 2
21+// ToolStatus represents the current state of a tool call.
25+ ToolStatusAwaitingPermission ToolStatus = iota
32+// ToolRenderOpts contains the data needed to render a tool call.
33+type ToolRenderOpts struct {
34+ ToolCall message.ToolCall
35+ Result *message.ToolResult
40+ PermissionRequested bool
41+ PermissionGranted bool
44+// Status returns the current status of the tool call.
45+func (opts *ToolRenderOpts) Status() ToolStatus {
47+ return ToolStatusCanceled
49+ if opts.Result != nil {
50+ if opts.Result.IsError {
51+ return ToolStatusError
53+ return ToolStatusSuccess
55+ if opts.PermissionRequested && !opts.PermissionGranted {
56+ return ToolStatusAwaitingPermission
58+ return ToolStatusRunning
61+// ToolRenderFunc is a function that renders a tool call to a string.
62+type ToolRenderFunc func(sty *styles.Styles, width int, t *ToolRenderOpts) string
64+// DefaultToolRenderer is a placeholder renderer for tools without a custom renderer.
65+func DefaultToolRenderer(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
66+ return "TODO: Implement Tool Renderer For: " + opts.ToolCall.Name
69+// ToolMessageItem represents a tool call message that can be displayed in the UI.
70+type ToolMessageItem struct {
71+ *highlightableMessageItem
73+ *focusableMessageItem
75+ renderFunc ToolRenderFunc
76+ toolCall message.ToolCall
77+ result *message.ToolResult
79+ // we use this so we can efficiently cache
80+ // tools that have a capped width (e.x bash.. and others)
88+// NewToolMessageItem creates a new tool message item with the given renderFunc.
89+func NewToolMessageItem(
91+ renderFunc ToolRenderFunc,
92+ toolCall message.ToolCall,
93+ result *message.ToolResult,
97+ t := &ToolMessageItem{
98+ highlightableMessageItem: defaultHighlighter(sty),
99+ cachedMessageItem: &cachedMessageItem{},
100+ focusableMessageItem: &focusableMessageItem{},
102+ renderFunc: renderFunc,
106+ hasCappedWidth: hasCappedWidth,
108+ t.anim = anim.New(anim.Settings{
111+ GradColorA: sty.Primary,
112+ GradColorB: sty.Secondary,
113+ LabelColor: sty.FgBase,
119+// ID returns the unique identifier for this tool message item.
120+func (t *ToolMessageItem) ID() string {
121+ return t.toolCall.ID
124+// StartAnimation starts the assistant message animation if it should be spinning.
125+func (t *ToolMessageItem) StartAnimation() tea.Cmd {
126+ if !t.isSpinning() {
129+ return t.anim.Start()
132+// Animate progresses the assistant message animation if it should be spinning.
133+func (t *ToolMessageItem) Animate(msg anim.StepMsg) tea.Cmd {
134+ if !t.isSpinning() {
137+ return t.anim.Animate(msg)
140+// Render renders the tool message item at the given width.
141+func (t *ToolMessageItem) Render(width int) string {
142+ toolItemWidth := width - messageLeftPaddingTotal
143+ if t.hasCappedWidth {
144+ toolItemWidth = cappedMessageWidth(width)
146+ style := t.sty.Chat.Message.ToolCallBlurred
148+ style = t.sty.Chat.Message.ToolCallFocused
151+ content, height, ok := t.getCachedRender(toolItemWidth)
152+ // if we are spinning or there is no cache rerender
153+ if !ok || t.isSpinning() {
154+ content = t.renderFunc(t.sty, toolItemWidth, &ToolRenderOpts{
155+ ToolCall: t.toolCall,
157+ Canceled: t.canceled,
159+ Expanded: t.expanded,
160+ IsSpinning: t.isSpinning(),
162+ height = lipgloss.Height(content)
163+ // cache the rendered content
164+ t.setCachedRender(content, toolItemWidth, height)
167+ highlightedContent := t.renderHighlighted(content, toolItemWidth, height)
168+ return style.Render(highlightedContent)
171+// isSpinning returns true if the tool should show animation.
172+func (t *ToolMessageItem) isSpinning() bool {
173+ return !t.toolCall.Finished && !t.canceled
176+// ToggleExpanded toggles the expanded state of the thinking box.
177+func (t *ToolMessageItem) ToggleExpanded() {
178+ t.expanded = !t.expanded
182+// HandleMouseClick implements MouseClickable.
183+func (t *ToolMessageItem) HandleMouseClick(btn ansi.MouseButton, x, y int) bool {
184+ if btn != ansi.MouseLeft {
191+// pendingTool renders a tool that is still in progress with an animation.
192+func pendingTool(sty *styles.Styles, name string, anim *anim.Anim) string {
193+ icon := sty.Tool.IconPending.Render()
194+ toolName := sty.Tool.NameNormal.Render(name)
198+ animView = anim.Render()
201+ return fmt.Sprintf("%s %s %s", icon, toolName, animView)
204+// toolEarlyStateContent handles error/cancelled/pending states before content rendering.
205+// Returns the rendered output and true if early state was handled.
206+func toolEarlyStateContent(sty *styles.Styles, opts *ToolRenderOpts, width int) (string, bool) {
208+ switch opts.Status() {
209+ case ToolStatusError:
210+ msg = toolErrorContent(sty, opts.Result, width)
211+ case ToolStatusCanceled:
212+ msg = sty.Tool.StateCancelled.Render("Canceled.")
213+ case ToolStatusAwaitingPermission:
214+ msg = sty.Tool.StateWaiting.Render("Requesting permission...")
215+ case ToolStatusRunning:
216+ msg = sty.Tool.StateWaiting.Render("Waiting for tool response...")
223+// toolErrorContent formats an error message with ERROR tag.
224+func toolErrorContent(sty *styles.Styles, result *message.ToolResult, width int) string {
228+ errContent := strings.ReplaceAll(result.Content, "\n", " ")
229+ errTag := sty.Tool.ErrorTag.Render("ERROR")
230+ tagWidth := lipgloss.Width(errTag)
231+ errContent = ansi.Truncate(errContent, width-tagWidth-3, "…")
232+ return fmt.Sprintf("%s %s", errTag, sty.Tool.ErrorMessage.Render(errContent))
235+// toolIcon returns the status icon for a tool call.
236+// toolIcon returns the status icon for a tool call based on its status.
237+func toolIcon(sty *styles.Styles, status ToolStatus) string {
239+ case ToolStatusSuccess:
240+ return sty.Tool.IconSuccess.String()
241+ case ToolStatusError:
242+ return sty.Tool.IconError.String()
243+ case ToolStatusCanceled:
244+ return sty.Tool.IconCancelled.String()
246+ return sty.Tool.IconPending.String()
250+// toolParamList formats parameters as "main (key=value, ...)" with truncation.
251+// toolParamList formats tool parameters as "main (key=value, ...)" with truncation.
252+func toolParamList(sty *styles.Styles, params []string, width int) string {
253+ // minSpaceForMainParam is the min space required for the main param
254+ // if this is less that the value set we will only show the main param nothing else
255+ const minSpaceForMainParam = 30
256+ if len(params) == 0 {
260+ mainParam := params[0]
262+ // Build key=value pairs from remaining params (consecutive key, value pairs).
263+ var kvPairs []string
264+ for i := 1; i+1 < len(params); i += 2 {
265+ if params[i+1] != "" {
266+ kvPairs = append(kvPairs, fmt.Sprintf("%s=%s", params[i], params[i+1]))
270+ // Try to include key=value pairs if there's enough space.
272+ if len(kvPairs) > 0 {
273+ partsStr := strings.Join(kvPairs, ", ")
274+ if remaining := width - lipgloss.Width(partsStr) - 3; remaining >= minSpaceForMainParam {
275+ output = fmt.Sprintf("%s (%s)", mainParam, partsStr)
280+ output = ansi.Truncate(output, width, "…")
282+ return sty.Tool.ParamMain.Render(output)
285+// toolHeader builds the tool header line: "● ToolName params..."
286+func toolHeader(sty *styles.Styles, status ToolStatus, name string, width int, params ...string) string {
287+ icon := toolIcon(sty, status)
288+ toolName := sty.Tool.NameNested.Render(name)
289+ prefix := fmt.Sprintf("%s %s ", icon, toolName)
290+ prefixWidth := lipgloss.Width(prefix)
291+ remainingWidth := width - prefixWidth
292+ paramsStr := toolParamList(sty, params, remainingWidth)
293+ return prefix + paramsStr
296+// toolOutputPlainContent renders plain text with optional expansion support.
297+func toolOutputPlainContent(sty *styles.Styles, content string, width int, expanded bool) string {
298+ content = strings.ReplaceAll(content, "\r\n", "\n")
299+ content = strings.ReplaceAll(content, "\t", " ")
300+ content = strings.TrimSpace(content)
301+ lines := strings.Split(content, "\n")
303+ maxLines := responseContextHeight
305+ maxLines = len(lines) // Show all
309+ for i, ln := range lines {
314+ if lipgloss.Width(ln) > width {
315+ ln = ansi.Truncate(ln, width, "…")
317+ out = append(out, sty.Tool.ContentLine.Width(width).Render(ln))
320+ wasTruncated := len(lines) > responseContextHeight
322+ if !expanded && wasTruncated {
323+ out = append(out, sty.Tool.ContentTruncation.
325+ Render(fmt.Sprintf("… (%d lines) [click or space to expand]", len(lines)-responseContextHeight)))
328+ return strings.Join(out, "\n")