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 toolNameParts := strings.SplitN(opts.ToolCall.Name, "_", 3)
36 if len(toolNameParts) != 3 {
37 return toolErrorContent(sty, &message.ToolResult{Content: "Invalid tool name"}, width)
38 }
39 mcpName := prettyName(toolNameParts[1])
40 toolName := prettyName(toolNameParts[2])
41
42 mcpName = sty.Tool.MCPName.Render(mcpName)
43 toolName = sty.Tool.MCPToolName.Render(toolName)
44
45 name := fmt.Sprintf("%s %s %s", mcpName, sty.Tool.MCPArrow.String(), toolName)
46
47 if opts.IsPending() {
48 return pendingTool(sty, name, opts.Anim)
49 }
50
51 var params map[string]any
52 if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil {
53 return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width)
54 }
55
56 var toolParams []string
57 if len(params) > 0 {
58 parsed, _ := json.Marshal(params)
59 toolParams = append(toolParams, string(parsed))
60 }
61
62 header := toolHeader(sty, opts.Status, name, width, opts.Compact, toolParams...)
63 if opts.Compact {
64 return header
65 }
66
67 if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok {
68 return joinToolParts(header, earlyState)
69 }
70
71 if !opts.HasResult() || opts.Result.Content == "" {
72 return header
73 }
74
75 bodyWidth := width - toolBodyLeftPaddingTotal
76 // see if the result is json
77 var result json.RawMessage
78 var body string
79 if err := json.Unmarshal([]byte(opts.Result.Content), &result); err == nil {
80 prettyResult, err := json.MarshalIndent(result, "", " ")
81 if err == nil {
82 body = sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.json", string(prettyResult), 0, bodyWidth, opts.ExpandedContent))
83 } else {
84 body = sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
85 }
86 } else if looksLikeMarkdown(opts.Result.Content) {
87 body = sty.Tool.Body.Render(toolOutputCodeContent(sty, "result.md", opts.Result.Content, 0, bodyWidth, opts.ExpandedContent))
88 } else {
89 body = sty.Tool.Body.Render(toolOutputPlainContent(sty, opts.Result.Content, bodyWidth, opts.ExpandedContent))
90 }
91 return joinToolParts(header, body)
92}
93
94func prettyName(name string) string {
95 name = strings.ReplaceAll(name, "_", " ")
96 name = strings.ReplaceAll(name, "-", " ")
97 return stringext.Capitalize(name)
98}
99
100// looksLikeMarkdown checks if content appears to be markdown by looking for
101// common markdown patterns.
102func looksLikeMarkdown(content string) bool {
103 patterns := []string{
104 "# ", // headers
105 "## ", // headers
106 "**", // bold
107 "```", // code fence
108 "- ", // unordered list
109 "1. ", // ordered list
110 "> ", // blockquote
111 "---", // horizontal rule
112 "***", // horizontal rule
113 }
114 for _, p := range patterns {
115 if strings.Contains(content, p) {
116 return true
117 }
118 }
119 return false
120}