1package chat
2
3import (
4 "encoding/json"
5 "fmt"
6 "strings"
7
8 "github.com/charmbracelet/crush/internal/message"
9 "github.com/charmbracelet/crush/internal/stringext"
10 "github.com/charmbracelet/crush/internal/ui/styles"
11)
12
13// MCPToolMessageItem is a message item that represents a bash tool call.
14type MCPToolMessageItem struct {
15 *baseToolMessageItem
16}
17
18var _ ToolMessageItem = (*MCPToolMessageItem)(nil)
19
20// NewMCPToolMessageItem creates a new [MCPToolMessageItem].
21func NewMCPToolMessageItem(
22 sty *styles.Styles,
23 toolCall message.ToolCall,
24 result *message.ToolResult,
25 canceled bool,
26) ToolMessageItem {
27 return newBaseToolMessageItem(sty, toolCall, result, &MCPToolRenderContext{}, canceled)
28}
29
30// MCPToolRenderContext renders bash tool messages.
31type MCPToolRenderContext struct{}
32
33// RenderTool implements the [ToolRenderer] interface.
34func (b *MCPToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
35 cappedWidth := cappedMessageWidth(width)
36 toolNameParts := strings.SplitN(opts.ToolCall.Name, "_", 3)
37 if len(toolNameParts) != 3 {
38 return toolErrorContent(sty, &message.ToolResult{Content: "Invalid tool name"}, cappedWidth)
39 }
40 mcpName := prettyName(toolNameParts[1])
41 toolName := prettyName(toolNameParts[2])
42
43 mcpName = sty.Tool.MCPName.Render(mcpName)
44 toolName = sty.Tool.MCPToolName.Render(toolName)
45
46 name := fmt.Sprintf("%s %s %s", mcpName, sty.Tool.MCPArrow.String(), toolName)
47
48 if opts.IsPending() {
49 return pendingTool(sty, name, opts.Anim)
50 }
51
52 var params map[string]any
53 if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil {
54 return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, cappedWidth)
55 }
56
57 var toolParams []string
58 if len(params) > 0 {
59 parsed, _ := json.Marshal(params)
60 toolParams = append(toolParams, string(parsed))
61 }
62
63 header := toolHeader(sty, opts.Status, name, cappedWidth, opts.Compact, toolParams...)
64 if opts.Compact {
65 return header
66 }
67
68 if earlyState, ok := toolEarlyStateContent(sty, opts, cappedWidth); ok {
69 return joinToolParts(header, earlyState)
70 }
71
72 if !opts.HasResult() || opts.Result.Content == "" {
73 return header
74 }
75
76 bodyWidth := cappedWidth - toolBodyLeftPaddingTotal
77 // see if the result is json
78 var result json.RawMessage
79 var body string
80 if err := json.Unmarshal([]byte(opts.Result.Content), &result); err == nil {
81 prettyResult, err := json.MarshalIndent(result, "", " ")
82 if err == nil {
83 body = sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.json", string(prettyResult), 0, bodyWidth, opts.ExpandedContent))
84 } else {
85 body = sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
86 }
87 } else if looksLikeMarkdown(opts.Result.Content) {
88 body = sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.md", opts.Result.Content, 0, bodyWidth, opts.ExpandedContent))
89 } else {
90 body = sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
91 }
92 return joinToolParts(header, body)
93}
94
95func prettyName(name string) string {
96 name = strings.ReplaceAll(name, "_", " ")
97 name = strings.ReplaceAll(name, "-", " ")
98 return stringext.Capitalize(name)
99}
100
101// looksLikeMarkdown checks if content appears to be markdown by looking for
102// common markdown patterns.
103func looksLikeMarkdown(content string) bool {
104 patterns := []string{
105 "# ", // headers
106 "## ", // headers
107 "**", // bold
108 "```", // code fence
109 "- ", // unordered list
110 "1. ", // ordered list
111 "> ", // blockquote
112 "---", // horizontal rule
113 "***", // horizontal rule
114 }
115 for _, p := range patterns {
116 if strings.Contains(content, p) {
117 return true
118 }
119 }
120 return false
121}