1package chat
2
3import (
4 "cmp"
5 "encoding/json"
6 "fmt"
7 "strings"
8
9 "charm.land/lipgloss/v2"
10 "github.com/charmbracelet/crush/internal/agent/tools"
11 "github.com/charmbracelet/crush/internal/message"
12 "github.com/charmbracelet/crush/internal/ui/styles"
13 "github.com/charmbracelet/x/ansi"
14)
15
16// -----------------------------------------------------------------------------
17// Bash Tool
18// -----------------------------------------------------------------------------
19
20// BashToolMessageItem is a message item that represents a bash tool call.
21type BashToolMessageItem struct {
22 *baseToolMessageItem
23}
24
25var _ ToolMessageItem = (*BashToolMessageItem)(nil)
26
27// NewBashToolMessageItem creates a new [BashToolMessageItem].
28func NewBashToolMessageItem(
29 sty *styles.Styles,
30 toolCall message.ToolCall,
31 result *message.ToolResult,
32 canceled bool,
33) ToolMessageItem {
34 return newBaseToolMessageItem(sty, toolCall, result, &BashToolRenderContext{}, canceled)
35}
36
37// BashToolRenderContext renders bash tool messages.
38type BashToolRenderContext struct{}
39
40// RenderTool implements the [ToolRenderer] interface.
41func (b *BashToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
42 if opts.IsPending() {
43 return pendingTool(sty, "Bash", opts.Anim)
44 }
45
46 var params tools.BashParams
47 if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil {
48 params.Command = "failed to parse command"
49 }
50
51 // Check if this is a background job.
52 var meta tools.BashResponseMetadata
53 if opts.HasResult() {
54 _ = json.Unmarshal([]byte(opts.Result.Metadata), &meta)
55 }
56
57 if meta.Background {
58 description := cmp.Or(meta.Description, params.Command)
59 content := "Command: " + params.Command + "\n" + opts.Result.Content
60 return renderJobTool(sty, opts, width, "Start", meta.ShellID, description, content)
61 }
62
63 // Regular bash command.
64 cmd := strings.ReplaceAll(params.Command, "\n", " ")
65 cmd = strings.ReplaceAll(cmd, "\t", " ")
66 toolParams := []string{cmd}
67 if params.RunInBackground {
68 toolParams = append(toolParams, "background", "true")
69 }
70
71 header := toolHeader(sty, opts.Status, "Bash", width, opts.Compact, toolParams...)
72 if opts.Compact {
73 return header
74 }
75
76 if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok {
77 return joinToolParts(header, earlyState)
78 }
79
80 if !opts.HasResult() {
81 return header
82 }
83
84 output := meta.Output
85 if output == "" && opts.Result.Content != tools.BashNoOutput {
86 output = opts.Result.Content
87 }
88 if output == "" {
89 return header
90 }
91
92 bodyWidth := width - toolBodyLeftPaddingTotal
93 body := sty.Tool.Body.Render(toolOutputPlainContent(sty, output, bodyWidth, opts.ExpandedContent))
94 return joinToolParts(header, body)
95}
96
97// -----------------------------------------------------------------------------
98// Job Output Tool
99// -----------------------------------------------------------------------------
100
101// JobOutputToolMessageItem is a message item for job_output tool calls.
102type JobOutputToolMessageItem struct {
103 *baseToolMessageItem
104}
105
106var _ ToolMessageItem = (*JobOutputToolMessageItem)(nil)
107
108// NewJobOutputToolMessageItem creates a new [JobOutputToolMessageItem].
109func NewJobOutputToolMessageItem(
110 sty *styles.Styles,
111 toolCall message.ToolCall,
112 result *message.ToolResult,
113 canceled bool,
114) ToolMessageItem {
115 return newBaseToolMessageItem(sty, toolCall, result, &JobOutputToolRenderContext{}, canceled)
116}
117
118// JobOutputToolRenderContext renders job_output tool messages.
119type JobOutputToolRenderContext struct{}
120
121// RenderTool implements the [ToolRenderer] interface.
122func (j *JobOutputToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
123 if opts.IsPending() {
124 return pendingTool(sty, "Job", opts.Anim)
125 }
126
127 var params tools.JobOutputParams
128 if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil {
129 return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width)
130 }
131
132 var description string
133 if opts.HasResult() && opts.Result.Metadata != "" {
134 var meta tools.JobOutputResponseMetadata
135 if err := json.Unmarshal([]byte(opts.Result.Metadata), &meta); err == nil {
136 description = cmp.Or(meta.Description, meta.Command)
137 }
138 }
139
140 content := ""
141 if opts.HasResult() {
142 content = opts.Result.Content
143 }
144 return renderJobTool(sty, opts, width, "Output", params.ShellID, description, content)
145}
146
147// -----------------------------------------------------------------------------
148// Job Kill Tool
149// -----------------------------------------------------------------------------
150
151// JobKillToolMessageItem is a message item for job_kill tool calls.
152type JobKillToolMessageItem struct {
153 *baseToolMessageItem
154}
155
156var _ ToolMessageItem = (*JobKillToolMessageItem)(nil)
157
158// NewJobKillToolMessageItem creates a new [JobKillToolMessageItem].
159func NewJobKillToolMessageItem(
160 sty *styles.Styles,
161 toolCall message.ToolCall,
162 result *message.ToolResult,
163 canceled bool,
164) ToolMessageItem {
165 return newBaseToolMessageItem(sty, toolCall, result, &JobKillToolRenderContext{}, canceled)
166}
167
168// JobKillToolRenderContext renders job_kill tool messages.
169type JobKillToolRenderContext struct{}
170
171// RenderTool implements the [ToolRenderer] interface.
172func (j *JobKillToolRenderContext) RenderTool(sty *styles.Styles, width int, opts *ToolRenderOpts) string {
173 if opts.IsPending() {
174 return pendingTool(sty, "Job", opts.Anim)
175 }
176
177 var params tools.JobKillParams
178 if err := json.Unmarshal([]byte(opts.ToolCall.Input), ¶ms); err != nil {
179 return toolErrorContent(sty, &message.ToolResult{Content: "Invalid parameters"}, width)
180 }
181
182 var description string
183 if opts.HasResult() && opts.Result.Metadata != "" {
184 var meta tools.JobKillResponseMetadata
185 if err := json.Unmarshal([]byte(opts.Result.Metadata), &meta); err == nil {
186 description = cmp.Or(meta.Description, meta.Command)
187 }
188 }
189
190 content := ""
191 if opts.HasResult() {
192 content = opts.Result.Content
193 }
194 return renderJobTool(sty, opts, width, "Kill", params.ShellID, description, content)
195}
196
197// renderJobTool renders a job-related tool with the common pattern:
198// header → nested check → early state → body.
199func renderJobTool(sty *styles.Styles, opts *ToolRenderOpts, width int, action, shellID, description, content string) string {
200 header := jobHeader(sty, opts.Status, action, shellID, description, width)
201 if opts.Compact {
202 return header
203 }
204
205 if earlyState, ok := toolEarlyStateContent(sty, opts, width); ok {
206 return joinToolParts(header, earlyState)
207 }
208
209 if content == "" {
210 return header
211 }
212
213 bodyWidth := width - toolBodyLeftPaddingTotal
214 body := sty.Tool.Body.Render(toolOutputPlainContent(sty, content, bodyWidth, opts.ExpandedContent))
215 return joinToolParts(header, body)
216}
217
218// jobHeader builds a header for job-related tools.
219// Format: "● Job (Action) PID shellID description..."
220func jobHeader(sty *styles.Styles, status ToolStatus, action, shellID, description string, width int) string {
221 icon := toolIcon(sty, status)
222 jobPart := sty.Tool.JobToolName.Render("Job")
223 actionPart := sty.Tool.JobAction.Render("(" + action + ")")
224 pidPart := sty.Tool.JobPID.Render("PID " + shellID)
225
226 prefix := fmt.Sprintf("%s %s %s %s", icon, jobPart, actionPart, pidPart)
227
228 if description == "" {
229 return prefix
230 }
231
232 prefixWidth := lipgloss.Width(prefix)
233 availableWidth := width - prefixWidth - 1
234 if availableWidth < 10 {
235 return prefix
236 }
237
238 truncatedDesc := ansi.Truncate(description, availableWidth, "…")
239 return prefix + " " + sty.Tool.JobDescription.Render(truncatedDesc)
240}
241
242// joinToolParts joins header and body with a blank line separator.
243func joinToolParts(header, body string) string {
244 return strings.Join([]string{header, "", body}, "\n")
245}