@@ -6,6 +6,8 @@ import (
7 tea "charm.land/bubbletea/v2"
7 tea "charm.land/bubbletea/v2"
8 "charm.land/lipgloss/v2"
8 "charm.land/lipgloss/v2"
9+ "charm.land/lipgloss/v2/tree"
10+ "github.com/charmbracelet/crush/internal/agent"
9 "github.com/charmbracelet/crush/internal/agent/tools"
11 "github.com/charmbracelet/crush/internal/agent/tools"
10 "github.com/charmbracelet/crush/internal/message"
12 "github.com/charmbracelet/crush/internal/message"
11 "github.com/charmbracelet/crush/internal/ui/anim"
13 "github.com/charmbracelet/crush/internal/ui/anim"
@@ -38,14 +40,27 @@ type ToolMessageItem interface {
38 ToolCall() message.ToolCall
40 ToolCall() message.ToolCall
39 SetToolCall(tc message.ToolCall)
41 SetToolCall(tc message.ToolCall)
40 SetResult(res *message.ToolResult)
42 SetResult(res *message.ToolResult)
44+ SetMessageID(id string)
43−// Simplifiable is an interface for tool items that can render in a simplified mode.
47+// Compactable is an interface for tool items that can render in a compacted mode.
44−// When simple mode is enabled, tools render as a compact single-line header.
48+// When compact mode is enabled, tools render as a compact single-line header.
45−type Simplifiable interface {
49+type Compactable interface {
46− SetSimple(simple bool)
50+ SetCompact(compact bool)
53+// IsSpinningState contains the state passed to IsSpinningFn for custom spinning logic.
54+type IsSpinningState struct {
55+ ToolCall message.ToolCall
56+ Result *message.ToolResult
60+// IsSpinningFn is a function type for custom spinning logic.
61+// Returns true if the tool should show the spinning animation.
62+type IsSpinningFn func(state IsSpinningState) bool
49// DefaultToolRenderContext implements the default [ToolRenderer] interface.
64// DefaultToolRenderContext implements the default [ToolRenderer] interface.
50type DefaultToolRenderContext struct{}
65type DefaultToolRenderContext struct{}
@@ -60,8 +75,8 @@ type ToolRenderOpts struct {
60 Result *message.ToolResult
75 Result *message.ToolResult
61 Canceled bool
76 Canceled bool
62 Anim *anim.Anim
77 Anim *anim.Anim
63− Expanded bool
78+ ExpandedContent bool
64− Simple bool
79+ Compact bool
65 IsSpinning bool
80 IsSpinning bool
66 PermissionRequested bool
81 PermissionRequested bool
67 PermissionGranted bool
82 PermissionGranted bool
@@ -106,18 +121,22 @@ type baseToolMessageItem struct {
106 toolRenderer ToolRenderer
121 toolRenderer ToolRenderer
107 toolCall message.ToolCall
122 toolCall message.ToolCall
108 result *message.ToolResult
123 result *message.ToolResult
109 canceled bool
125 canceled bool
110 permissionRequested bool
126 permissionRequested bool
111 permissionGranted bool
127 permissionGranted bool
112 // we use this so we can efficiently cache
128 // we use this so we can efficiently cache
113 // tools that have a capped width (e.x bash.. and others)
129 // tools that have a capped width (e.x bash.. and others)
114 hasCappedWidth bool
130 hasCappedWidth bool
115− // isSimple indicates this tool should render in simplified/compact mode.
131+ // isCompact indicates this tool should render in compact mode.
116− isSimple bool
132+ isCompact bool
133+ // isSpinningFn allows tools to override the default spinning logic.
134+ // If nil, uses the default: !toolCall.Finished && !canceled.
135+ isSpinningFn IsSpinningFn
118− sty *styles.Styles
137+ sty *styles.Styles
119− anim *anim.Anim
138+ anim *anim.Anim
120− expanded bool
139+ expandedContent bool
123// newBaseToolMessageItem is the internal constructor for base tool message items.
142// newBaseToolMessageItem is the internal constructor for base tool message items.
@@ -157,45 +176,58 @@ func newBaseToolMessageItem(
157// NewToolMessageItem creates a new [ToolMessageItem] based on the tool call name.
176// NewToolMessageItem creates a new [ToolMessageItem] based on the tool call name.
159// It returns a specific tool message item type if implemented, otherwise it
178// It returns a specific tool message item type if implemented, otherwise it
160−// returns a generic tool message item.
179+// returns a generic tool message item. The messageID is the ID of the assistant
180+// message containing this tool call.
161func NewToolMessageItem(
181func NewToolMessageItem(
162 sty *styles.Styles,
182 sty *styles.Styles,
163 toolCall message.ToolCall,
184 toolCall message.ToolCall,
164 result *message.ToolResult,
185 result *message.ToolResult,
165 canceled bool,
186 canceled bool,
166) ToolMessageItem {
187) ToolMessageItem {
188+ var item ToolMessageItem
167 switch toolCall.Name {
189 switch toolCall.Name {
168 case tools.BashToolName:
190 case tools.BashToolName:
169− return NewBashToolMessageItem(sty, toolCall, result, canceled)
191+ item = NewBashToolMessageItem(sty, toolCall, result, canceled)
170 case tools.JobOutputToolName:
192 case tools.JobOutputToolName:
171− return NewJobOutputToolMessageItem(sty, toolCall, result, canceled)
193+ item = NewJobOutputToolMessageItem(sty, toolCall, result, canceled)
172 case tools.JobKillToolName:
194 case tools.JobKillToolName:
173− return NewJobKillToolMessageItem(sty, toolCall, result, canceled)
195+ item = NewJobKillToolMessageItem(sty, toolCall, result, canceled)
174 case tools.ViewToolName:
196 case tools.ViewToolName:
175− return NewViewToolMessageItem(sty, toolCall, result, canceled)
197+ item = NewViewToolMessageItem(sty, toolCall, result, canceled)
176 case tools.WriteToolName:
198 case tools.WriteToolName:
177− return NewWriteToolMessageItem(sty, toolCall, result, canceled)
199+ item = NewWriteToolMessageItem(sty, toolCall, result, canceled)
178 case tools.EditToolName:
200 case tools.EditToolName:
179− return NewEditToolMessageItem(sty, toolCall, result, canceled)
201+ item = NewEditToolMessageItem(sty, toolCall, result, canceled)
180 case tools.MultiEditToolName:
202 case tools.MultiEditToolName:
181− return NewMultiEditToolMessageItem(sty, toolCall, result, canceled)
203+ item = NewMultiEditToolMessageItem(sty, toolCall, result, canceled)
182 case tools.GlobToolName:
204 case tools.GlobToolName:
183− return NewGlobToolMessageItem(sty, toolCall, result, canceled)
205+ item = NewGlobToolMessageItem(sty, toolCall, result, canceled)
184 case tools.GrepToolName:
206 case tools.GrepToolName:
185− return NewGrepToolMessageItem(sty, toolCall, result, canceled)
207+ item = NewGrepToolMessageItem(sty, toolCall, result, canceled)
186 case tools.LSToolName:
208 case tools.LSToolName:
187− return NewLSToolMessageItem(sty, toolCall, result, canceled)
209+ item = NewLSToolMessageItem(sty, toolCall, result, canceled)
188 case tools.DownloadToolName:
210 case tools.DownloadToolName:
189− return NewDownloadToolMessageItem(sty, toolCall, result, canceled)
211+ item = NewDownloadToolMessageItem(sty, toolCall, result, canceled)
190 case tools.FetchToolName:
212 case tools.FetchToolName:
191− return NewFetchToolMessageItem(sty, toolCall, result, canceled)
213+ item = NewFetchToolMessageItem(sty, toolCall, result, canceled)
192 case tools.SourcegraphToolName:
214 case tools.SourcegraphToolName:
193− return NewSourcegraphToolMessageItem(sty, toolCall, result, canceled)
215+ item = NewSourcegraphToolMessageItem(sty, toolCall, result, canceled)
194 case tools.DiagnosticsToolName:
216 case tools.DiagnosticsToolName:
195− return NewDiagnosticsToolMessageItem(sty, toolCall, result, canceled)
217+ item = NewDiagnosticsToolMessageItem(sty, toolCall, result, canceled)
218+ case agent.AgentToolName:
219+ item = NewAgentToolMessageItem(sty, toolCall, result, canceled)
220+ case tools.AgenticFetchToolName:
221+ item = NewAgenticFetchToolMessageItem(sty, toolCall, result, canceled)
222+ case tools.WebFetchToolName:
223+ item = NewWebFetchToolMessageItem(sty, toolCall, result, canceled)
224+ case tools.WebSearchToolName:
225+ item = NewWebSearchToolMessageItem(sty, toolCall, result, canceled)
226+ case tools.TodosToolName:
227+ item = NewTodosToolMessageItem(sty, toolCall, result, canceled)
197 // TODO: Implement other tool items
229 // TODO: Implement other tool items
198− return newBaseToolMessageItem(
230+ item = newBaseToolMessageItem(
200 toolCall,
232 toolCall,
@@ -203,11 +235,13 @@ func NewToolMessageItem(
203 canceled,
235 canceled,
238+ item.SetMessageID(messageID)
208−// SetSimple implements the Simplifiable interface.
242+// SetCompact implements the Compactable interface.
209−func (t *baseToolMessageItem) SetSimple(simple bool) {
243+func (t *baseToolMessageItem) SetCompact(compact bool) {
210− t.isSimple = simple
244+ t.isCompact = compact
211 t.clearCache()
245 t.clearCache()
@@ -243,6 +277,10 @@ func (t *baseToolMessageItem) Render(width int) string {
243 style = t.sty.Chat.Message.ToolCallFocused
277 style = t.sty.Chat.Message.ToolCallFocused
281+ style = t.sty.Chat.Message.ToolCallCompact
246 content, height, ok := t.getCachedRender(toolItemWidth)
284 content, height, ok := t.getCachedRender(toolItemWidth)
247 // if we are spinning or there is no cache rerender
285 // if we are spinning or there is no cache rerender
248 if !ok || t.isSpinning() {
286 if !ok || t.isSpinning() {
@@ -251,8 +289,8 @@ func (t *baseToolMessageItem) Render(width int) string {
251 Result: t.result,
289 Result: t.result,
252 Canceled: t.canceled,
290 Canceled: t.canceled,
253 Anim: t.anim,
291 Anim: t.anim,
254− Expanded: t.expanded,
292+ ExpandedContent: t.expandedContent,
255− Simple: t.isSimple,
293+ Compact: t.isCompact,
256 PermissionRequested: t.permissionRequested,
294 PermissionRequested: t.permissionRequested,
257 PermissionGranted: t.permissionGranted,
295 PermissionGranted: t.permissionGranted,
258 IsSpinning: t.isSpinning(),
296 IsSpinning: t.isSpinning(),
@@ -283,6 +321,16 @@ func (t *baseToolMessageItem) SetResult(res *message.ToolResult) {
283 t.clearCache()
321 t.clearCache()
324+// MessageID returns the ID of the message containing this tool call.
325+func (t *baseToolMessageItem) MessageID() string {
329+// SetMessageID sets the ID of the message containing this tool call.
330+func (t *baseToolMessageItem) SetMessageID(id string) {
286// SetPermissionRequested sets whether permission has been requested for this tool call.
334// SetPermissionRequested sets whether permission has been requested for this tool call.
287// TODO: Consider merging with SetPermissionGranted and add an interface for
335// TODO: Consider merging with SetPermissionGranted and add an interface for
288// permission management.
336// permission management.
@@ -301,12 +349,24 @@ func (t *baseToolMessageItem) SetPermissionGranted(granted bool) {
302// isSpinning returns true if the tool should show animation.
350// isSpinning returns true if the tool should show animation.
303func (t *baseToolMessageItem) isSpinning() bool {
351func (t *baseToolMessageItem) isSpinning() bool {
352+ if t.isSpinningFn != nil {
353+ return t.isSpinningFn(IsSpinningState{
354+ ToolCall: t.toolCall,
356+ Canceled: t.canceled,
304 return !t.toolCall.Finished && !t.canceled
359 return !t.toolCall.Finished && !t.canceled
362+// SetIsSpinningFn sets a custom function to determine if the tool should spin.
363+func (t *baseToolMessageItem) SetIsSpinningFn(fn IsSpinningFn) {
307// ToggleExpanded toggles the expanded state of the thinking box.
367// ToggleExpanded toggles the expanded state of the thinking box.
308func (t *baseToolMessageItem) ToggleExpanded() {
368func (t *baseToolMessageItem) ToggleExpanded() {
309− t.expanded = !t.expanded
369+ t.expandedContent = !t.expandedContent
310 t.clearCache()
370 t.clearCache()
@@ -654,3 +714,62 @@ func toolOutputMultiEditDiffContent(sty *styles.Styles, file string, meta tools.
655 return sty.Tool.Body.Render(formatted)
715 return sty.Tool.Body.Render(formatted)
718+// roundedEnumerator creates a tree enumerator with rounded corners.
719+func roundedEnumerator(lPadding, width int) tree.Enumerator {
726+ return func(children tree.Children, index int) string {
727+ line := strings.Repeat("─", width)
728+ padding := strings.Repeat(" ", lPadding)
729+ if children.Length()-1 == index {
730+ return padding + "╰" + line
732+ return padding + "├" + line
736+// toolOutputMarkdownContent renders markdown content with optional truncation.
737+func toolOutputMarkdownContent(sty *styles.Styles, content string, width int, expanded bool) string {
738+ content = strings.ReplaceAll(content, "\r\n", "\n")
739+ content = strings.ReplaceAll(content, "\t", " ")
740+ content = strings.TrimSpace(content)
742+ // Cap width for readability.
747+ renderer := common.PlainMarkdownRenderer(sty, width)
748+ rendered, err := renderer.Render(content)
750+ return toolOutputPlainContent(sty, content, width, expanded)
753+ lines := strings.Split(rendered, "\n")
754+ maxLines := responseContextHeight
756+ maxLines = len(lines)
760+ for i, ln := range lines {
764+ out = append(out, ln)
767+ if len(lines) > maxLines && !expanded {
768+ out = append(out, sty.Tool.ContentTruncation.
770+ Render(fmt.Sprintf(assistantMessageTruncateFormat, len(lines)-maxLines)),
774+ return sty.Tool.Body.Render(strings.Join(out, "\n"))