bash.go

  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), &params)
 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}