1package chat
2
3import (
4 "encoding/json"
5 "strings"
6
7 "github.com/charmbracelet/crush/internal/agent/tools"
8 "github.com/charmbracelet/crush/internal/message"
9 "github.com/charmbracelet/crush/internal/ui/styles"
10)
11
12// BashToolMessageItem is a message item that represents a bash tool call.
13type BashToolMessageItem struct {
14 *baseToolMessageItem
15}
16
17var _ ToolMessageItem = (*BashToolMessageItem)(nil)
18
19// NewBashToolMessageItem creates a new [BashToolMessageItem].
20func NewBashToolMessageItem(
21 sty *styles.Styles,
22 toolCall message.ToolCall,
23 result *message.ToolResult,
24 canceled bool,
25) ToolMessageItem {
26 return newBaseToolMessageItem(
27 sty,
28 toolCall,
29 result,
30 &BashToolRenderContext{},
31 canceled,
32 )
33}
34
35// BashToolRenderContext holds context for rendering bash tool messages.
36//
37// It implements the [ToolRenderer] interface.
38type BashToolRenderContext struct{}
39
40// RenderTool implements the [ToolRenderer] interface.
41func (b *BashToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
42 cappedWidth := cappedMessageWidth(width)
43 const toolName = "Bash"
44 if !opts.ToolCall.Finished && !opts.Canceled {
45 return pendingTool(sty, toolName, opts.Anim)
46 }
47
48 var params tools.BashParams
49 var cmd string
50 err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms)
51
52 if err != nil {
53 cmd = "failed to parse command"
54 } else {
55 cmd = strings.ReplaceAll(params.Command, "\n", " ")
56 cmd = strings.ReplaceAll(cmd, "\t", " ")
57 }
58
59 // TODO: if the tool is being run in the background use the background job renderer
60
61 toolParams := []string{
62 cmd,
63 }
64
65 if params.RunInBackground {
66 toolParams = append(toolParams, "background", "true")
67 }
68
69 header := toolHeader(sty, opts.Status(), "Bash", cappedWidth, toolParams...)
70
71 if opts.Nested {
72 return header
73 }
74
75 earlyStateContent, ok := toolEarlyStateContent(sty, opts, cappedWidth)
76
77 // If this is OK that means that the tool is not done yet or it was canceled
78 if ok {
79 return strings.Join([]string{header, "", earlyStateContent}, "\n")
80 }
81
82 if opts.Result == nil {
83 // We should not get here!
84 return header
85 }
86
87 var meta tools.BashResponseMetadata
88 err = json.Unmarshal([]byte(opts.Result.Metadata), &meta)
89
90 var output string
91 if err != nil {
92 output = "failed to parse output"
93 }
94 output = meta.Output
95 if output == "" && opts.Result.Content != tools.BashNoOutput {
96 output = opts.Result.Content
97 }
98
99 if output == "" {
100 return header
101 }
102
103 bodyWidth := cappedWidth - toolBodyLeftPaddingTotal
104
105 output = sty.Tool.Body.Render(toolOutputPlainContent(sty, output, bodyWidth, opts.Expanded))
106
107 return strings.Join([]string{header, "", output}, "\n")
108}