1package chat
2
3import (
4 "cmp"
5 "fmt"
6 "strings"
7 "time"
8
9 tea "charm.land/bubbletea/v2"
10 "charm.land/lipgloss/v2"
11 "charm.land/lipgloss/v2/tree"
12 "github.com/charmbracelet/crush/internal/agent"
13 "github.com/charmbracelet/crush/internal/agent/tools"
14 "github.com/charmbracelet/crush/internal/fsext"
15 "github.com/charmbracelet/crush/internal/ui/list"
16)
17
18// NewToolItem creates the appropriate tool item for the given context.
19func NewToolItem(ctx ToolCallContext) MessageItem {
20 switch ctx.Call.Name {
21 // Bash tools
22 case tools.BashToolName:
23 return NewBashToolItem(ctx)
24 case tools.JobOutputToolName:
25 return NewJobOutputToolItem(ctx)
26 case tools.JobKillToolName:
27 return NewJobKillToolItem(ctx)
28
29 // File tools
30 case tools.ViewToolName:
31 return NewViewToolItem(ctx)
32 case tools.EditToolName:
33 return NewEditToolItem(ctx)
34 case tools.MultiEditToolName:
35 return NewMultiEditToolItem(ctx)
36 case tools.WriteToolName:
37 return NewWriteToolItem(ctx)
38
39 // Search tools
40 case tools.GlobToolName:
41 return NewGlobToolItem(ctx)
42 case tools.GrepToolName:
43 return NewGrepToolItem(ctx)
44 case tools.LSToolName:
45 return NewLSToolItem(ctx)
46 case tools.SourcegraphToolName:
47 return NewSourcegraphToolItem(ctx)
48
49 // Fetch tools
50 case tools.FetchToolName:
51 return NewFetchToolItem(ctx)
52 case tools.AgenticFetchToolName:
53 return NewAgenticFetchToolItem(ctx)
54 case tools.WebFetchToolName:
55 return NewWebFetchToolItem(ctx)
56 case tools.WebSearchToolName:
57 return NewWebSearchToolItem(ctx)
58 case tools.DownloadToolName:
59 return NewDownloadToolItem(ctx)
60
61 // LSP tools
62 case tools.DiagnosticsToolName:
63 return NewDiagnosticsToolItem(ctx)
64 case tools.ReferencesToolName:
65 return NewReferencesToolItem(ctx)
66
67 // Misc tools
68 case tools.TodosToolName:
69 return NewTodosToolItem(ctx)
70 case agent.AgentToolName:
71 return NewAgentToolItem(ctx)
72
73 default:
74 return NewGenericToolItem(ctx)
75 }
76}
77
78// -----------------------------------------------------------------------------
79// Bash Tools
80// -----------------------------------------------------------------------------
81
82// BashToolItem renders bash command execution.
83type BashToolItem struct {
84 toolItem
85}
86
87func NewBashToolItem(ctx ToolCallContext) *BashToolItem {
88 return &BashToolItem{
89 toolItem: newToolItem(ctx),
90 }
91}
92
93// Update implements list.Updatable.
94func (m *BashToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
95 cmd, changed := m.updateAnimation(msg)
96 if changed {
97 return m, cmd
98 }
99 return m, nil
100}
101
102func (m *BashToolItem) Render(width int) string {
103 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
104 return m.renderPending()
105 }
106
107 var params tools.BashParams
108 unmarshalParams(m.ctx.Call.Input, ¶ms)
109
110 cmd := strings.ReplaceAll(params.Command, "\n", " ")
111 cmd = strings.ReplaceAll(cmd, "\t", " ")
112
113 if m.ctx.Call.Finished && m.ctx.HasResult() {
114 var meta tools.BashResponseMetadata
115 unmarshalParams(m.ctx.Result.Metadata, &meta)
116 if meta.Background {
117 return m.renderBackgroundJob(params, meta, width)
118 }
119 }
120
121 args := NewParamBuilder().
122 Main(cmd).
123 Flag("background", params.RunInBackground).
124 Build()
125
126 header := renderToolHeader(&m.ctx, "Bash", width, args...)
127
128 if result, done := renderEarlyState(&m.ctx, header, width); done {
129 return result
130 }
131
132 var meta tools.BashResponseMetadata
133 unmarshalParams(m.ctx.Result.Metadata, &meta)
134
135 output := meta.Output
136 if output == "" && m.ctx.Result.Content != tools.BashNoOutput {
137 output = m.ctx.Result.Content
138 }
139
140 if output == "" {
141 return header
142 }
143
144 body := renderPlainContent(output, width-2, m.ctx.Styles, &m.toolItem)
145 return joinHeaderBody(header, body, m.ctx.Styles)
146}
147
148func (m *BashToolItem) renderBackgroundJob(params tools.BashParams, meta tools.BashResponseMetadata, width int) string {
149 description := cmp.Or(meta.Description, params.Command)
150 header := renderJobHeader(&m.ctx, "Start", meta.ShellID, description, width)
151
152 if m.ctx.IsNested {
153 return header
154 }
155
156 if result, done := renderEarlyState(&m.ctx, header, width); done {
157 return result
158 }
159
160 content := "Command: " + params.Command + "\n" + m.ctx.Result.Content
161 body := renderPlainContent(content, width-2, m.ctx.Styles, &m.toolItem)
162 return joinHeaderBody(header, body, m.ctx.Styles)
163}
164
165// JobOutputToolItem renders job output retrieval.
166type JobOutputToolItem struct {
167 toolItem
168}
169
170func NewJobOutputToolItem(ctx ToolCallContext) *JobOutputToolItem {
171 return &JobOutputToolItem{
172 toolItem: newToolItem(ctx),
173 }
174}
175
176func (m *JobOutputToolItem) Render(width int) string {
177 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
178 return m.renderPending()
179 }
180
181 var params tools.JobOutputParams
182 unmarshalParams(m.ctx.Call.Input, ¶ms)
183
184 var meta tools.JobOutputResponseMetadata
185 var description string
186 if m.ctx.Result != nil && m.ctx.Result.Metadata != "" {
187 unmarshalParams(m.ctx.Result.Metadata, &meta)
188 description = cmp.Or(meta.Description, meta.Command)
189 }
190
191 header := renderJobHeader(&m.ctx, "Output", params.ShellID, description, width)
192
193 if m.ctx.IsNested {
194 return header
195 }
196
197 if result, done := renderEarlyState(&m.ctx, header, width); done {
198 return result
199 }
200
201 body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
202 return joinHeaderBody(header, body, m.ctx.Styles)
203}
204
205// JobKillToolItem renders job termination.
206type JobKillToolItem struct {
207 toolItem
208}
209
210func NewJobKillToolItem(ctx ToolCallContext) *JobKillToolItem {
211 return &JobKillToolItem{
212 toolItem: newToolItem(ctx),
213 }
214}
215
216func (m *JobKillToolItem) Render(width int) string {
217 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
218 return m.renderPending()
219 }
220
221 var params tools.JobKillParams
222 unmarshalParams(m.ctx.Call.Input, ¶ms)
223
224 var meta tools.JobKillResponseMetadata
225 var description string
226 if m.ctx.Result != nil && m.ctx.Result.Metadata != "" {
227 unmarshalParams(m.ctx.Result.Metadata, &meta)
228 description = cmp.Or(meta.Description, meta.Command)
229 }
230
231 header := renderJobHeader(&m.ctx, "Kill", params.ShellID, description, width)
232
233 if m.ctx.IsNested {
234 return header
235 }
236
237 if result, done := renderEarlyState(&m.ctx, header, width); done {
238 return result
239 }
240
241 body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
242 return joinHeaderBody(header, body, m.ctx.Styles)
243}
244
245// renderJobHeader builds a job-specific header with action and PID.
246func renderJobHeader(ctx *ToolCallContext, action, pid, description string, width int) string {
247 sty := ctx.Styles
248 icon := renderToolIcon(ctx.Status(), sty)
249
250 jobPart := sty.Tool.JobToolName.Render("Job")
251 actionPart := sty.Tool.JobAction.Render("(" + action + ")")
252 pidPart := sty.Tool.JobPID.Render("PID " + pid)
253
254 prefix := fmt.Sprintf("%s %s %s %s", icon, jobPart, actionPart, pidPart)
255
256 if description == "" {
257 return prefix
258 }
259
260 descPart := " " + sty.Tool.JobDescription.Render(description)
261 fullHeader := prefix + descPart
262
263 if lipgloss.Width(fullHeader) > width {
264 availableWidth := width - lipgloss.Width(prefix) - 1
265 if availableWidth < 10 {
266 return prefix
267 }
268 descPart = " " + sty.Tool.JobDescription.Render(truncateText(description, availableWidth))
269 fullHeader = prefix + descPart
270 }
271
272 return fullHeader
273}
274
275// -----------------------------------------------------------------------------
276// File Tools
277// -----------------------------------------------------------------------------
278
279// ViewToolItem renders file viewing with syntax highlighting.
280type ViewToolItem struct {
281 toolItem
282}
283
284func NewViewToolItem(ctx ToolCallContext) *ViewToolItem {
285 return &ViewToolItem{
286 toolItem: newToolItem(ctx),
287 }
288}
289
290func (m *ViewToolItem) Render(width int) string {
291 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
292 return m.renderPending()
293 }
294
295 var params tools.ViewParams
296 unmarshalParams(m.ctx.Call.Input, ¶ms)
297
298 file := fsext.PrettyPath(params.FilePath)
299 args := NewParamBuilder().
300 Main(file).
301 KeyValue("limit", formatNonZero(params.Limit)).
302 KeyValue("offset", formatNonZero(params.Offset)).
303 Build()
304
305 header := renderToolHeader(&m.ctx, "View", width, args...)
306
307 if result, done := renderEarlyState(&m.ctx, header, width); done {
308 return result
309 }
310
311 if m.ctx.Result.Data != "" && strings.HasPrefix(m.ctx.Result.MIMEType, "image/") {
312 body := renderImageContent(m.ctx.Result.Data, m.ctx.Result.MIMEType, "", m.ctx.Styles)
313 return joinHeaderBody(header, body, m.ctx.Styles)
314 }
315
316 var meta tools.ViewResponseMetadata
317 unmarshalParams(m.ctx.Result.Metadata, &meta)
318
319 body := renderCodeContent(meta.FilePath, meta.Content, params.Offset, width-2, m.ctx.Styles, &m.toolItem)
320 return joinHeaderBody(header, body, m.ctx.Styles)
321}
322
323// EditToolItem renders file editing with diff visualization.
324type EditToolItem struct {
325 toolItem
326}
327
328func NewEditToolItem(ctx ToolCallContext) *EditToolItem {
329 return &EditToolItem{
330 toolItem: newToolItem(ctx),
331 }
332}
333
334func (m *EditToolItem) Render(width int) string {
335 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
336 return m.renderPending()
337 }
338
339 var params tools.EditParams
340 unmarshalParams(m.ctx.Call.Input, ¶ms)
341
342 file := fsext.PrettyPath(params.FilePath)
343 args := NewParamBuilder().Main(file).Build()
344
345 header := renderToolHeader(&m.ctx, "Edit", width, args...)
346
347 if result, done := renderEarlyState(&m.ctx, header, width); done {
348 return result
349 }
350
351 var meta tools.EditResponseMetadata
352 if err := unmarshalParams(m.ctx.Result.Metadata, &meta); err != nil {
353 body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, nil)
354 return joinHeaderBody(header, body, m.ctx.Styles)
355 }
356
357 body := renderDiffContent(file, meta.OldContent, meta.NewContent, width-2, m.ctx.Styles, &m.toolItem)
358 return joinHeaderBody(header, body, m.ctx.Styles)
359}
360
361// MultiEditToolItem renders multiple file edits with diff visualization.
362type MultiEditToolItem struct {
363 toolItem
364}
365
366func NewMultiEditToolItem(ctx ToolCallContext) *MultiEditToolItem {
367 return &MultiEditToolItem{
368 toolItem: newToolItem(ctx),
369 }
370}
371
372func (m *MultiEditToolItem) Render(width int) string {
373 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
374 return m.renderPending()
375 }
376
377 var params tools.MultiEditParams
378 unmarshalParams(m.ctx.Call.Input, ¶ms)
379
380 file := fsext.PrettyPath(params.FilePath)
381 args := NewParamBuilder().
382 Main(file).
383 KeyValue("edits", fmt.Sprintf("%d", len(params.Edits))).
384 Build()
385
386 header := renderToolHeader(&m.ctx, "Multi-Edit", width, args...)
387
388 if result, done := renderEarlyState(&m.ctx, header, width); done {
389 return result
390 }
391
392 var meta tools.MultiEditResponseMetadata
393 if err := unmarshalParams(m.ctx.Result.Metadata, &meta); err != nil {
394 body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, nil)
395 return joinHeaderBody(header, body, m.ctx.Styles)
396 }
397
398 body := renderDiffContent(file, meta.OldContent, meta.NewContent, width-2, m.ctx.Styles, &m.toolItem)
399
400 if len(meta.EditsFailed) > 0 {
401 sty := m.ctx.Styles
402 noteTag := sty.Tool.NoteTag.Render("Note")
403 noteMsg := fmt.Sprintf("%d of %d edits succeeded", meta.EditsApplied, len(params.Edits))
404 note := fmt.Sprintf("%s %s", noteTag, sty.Tool.NoteMessage.Render(noteMsg))
405 body = lipgloss.JoinVertical(lipgloss.Left, body, "", note)
406 }
407
408 return joinHeaderBody(header, body, m.ctx.Styles)
409}
410
411// WriteToolItem renders file writing with syntax-highlighted content preview.
412type WriteToolItem struct {
413 toolItem
414}
415
416func NewWriteToolItem(ctx ToolCallContext) *WriteToolItem {
417 return &WriteToolItem{
418 toolItem: newToolItem(ctx),
419 }
420}
421
422func (m *WriteToolItem) Render(width int) string {
423 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
424 return m.renderPending()
425 }
426
427 var params tools.WriteParams
428 unmarshalParams(m.ctx.Call.Input, ¶ms)
429
430 file := fsext.PrettyPath(params.FilePath)
431 args := NewParamBuilder().Main(file).Build()
432
433 header := renderToolHeader(&m.ctx, "Write", width, args...)
434
435 if result, done := renderEarlyState(&m.ctx, header, width); done {
436 return result
437 }
438
439 body := renderCodeContent(file, params.Content, 0, width-2, m.ctx.Styles, &m.toolItem)
440 return joinHeaderBody(header, body, m.ctx.Styles)
441}
442
443// -----------------------------------------------------------------------------
444// Search Tools
445// -----------------------------------------------------------------------------
446
447// GlobToolItem renders glob file pattern matching results.
448type GlobToolItem struct {
449 toolItem
450}
451
452func NewGlobToolItem(ctx ToolCallContext) *GlobToolItem {
453 return &GlobToolItem{
454 toolItem: newToolItem(ctx),
455 }
456}
457
458func (m *GlobToolItem) Render(width int) string {
459 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
460 return m.renderPending()
461 }
462
463 var params tools.GlobParams
464 unmarshalParams(m.ctx.Call.Input, ¶ms)
465
466 args := NewParamBuilder().
467 Main(params.Pattern).
468 KeyValue("path", fsext.PrettyPath(params.Path)).
469 Build()
470
471 header := renderToolHeader(&m.ctx, "Glob", width, args...)
472
473 if result, done := renderEarlyState(&m.ctx, header, width); done {
474 return result
475 }
476
477 body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
478 return joinHeaderBody(header, body, m.ctx.Styles)
479}
480
481// GrepToolItem renders grep content search results.
482type GrepToolItem struct {
483 toolItem
484}
485
486func NewGrepToolItem(ctx ToolCallContext) *GrepToolItem {
487 return &GrepToolItem{
488 toolItem: newToolItem(ctx),
489 }
490}
491
492func (m *GrepToolItem) Render(width int) string {
493 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
494 return m.renderPending()
495 }
496
497 var params tools.GrepParams
498 unmarshalParams(m.ctx.Call.Input, ¶ms)
499
500 args := NewParamBuilder().
501 Main(params.Pattern).
502 KeyValue("path", fsext.PrettyPath(params.Path)).
503 KeyValue("include", params.Include).
504 Flag("literal", params.LiteralText).
505 Build()
506
507 header := renderToolHeader(&m.ctx, "Grep", width, args...)
508
509 if result, done := renderEarlyState(&m.ctx, header, width); done {
510 return result
511 }
512
513 body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
514 return joinHeaderBody(header, body, m.ctx.Styles)
515}
516
517// LSToolItem renders directory listing results.
518type LSToolItem struct {
519 toolItem
520}
521
522func NewLSToolItem(ctx ToolCallContext) *LSToolItem {
523 return &LSToolItem{
524 toolItem: newToolItem(ctx),
525 }
526}
527
528func (m *LSToolItem) Render(width int) string {
529 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
530 return m.renderPending()
531 }
532
533 var params tools.LSParams
534 unmarshalParams(m.ctx.Call.Input, ¶ms)
535
536 path := cmp.Or(params.Path, ".")
537 path = fsext.PrettyPath(path)
538
539 args := NewParamBuilder().Main(path).Build()
540 header := renderToolHeader(&m.ctx, "List", width, args...)
541
542 if result, done := renderEarlyState(&m.ctx, header, width); done {
543 return result
544 }
545
546 body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
547 return joinHeaderBody(header, body, m.ctx.Styles)
548}
549
550// SourcegraphToolItem renders code search results.
551type SourcegraphToolItem struct {
552 toolItem
553}
554
555func NewSourcegraphToolItem(ctx ToolCallContext) *SourcegraphToolItem {
556 return &SourcegraphToolItem{
557 toolItem: newToolItem(ctx),
558 }
559}
560
561func (m *SourcegraphToolItem) Render(width int) string {
562 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
563 return m.renderPending()
564 }
565
566 var params tools.SourcegraphParams
567 unmarshalParams(m.ctx.Call.Input, ¶ms)
568
569 args := NewParamBuilder().
570 Main(params.Query).
571 KeyValue("count", formatNonZero(params.Count)).
572 KeyValue("context", formatNonZero(params.ContextWindow)).
573 Build()
574
575 header := renderToolHeader(&m.ctx, "Sourcegraph", width, args...)
576
577 if result, done := renderEarlyState(&m.ctx, header, width); done {
578 return result
579 }
580
581 body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
582 return joinHeaderBody(header, body, m.ctx.Styles)
583}
584
585// -----------------------------------------------------------------------------
586// Fetch Tools
587// -----------------------------------------------------------------------------
588
589// FetchToolItem renders URL fetching with format-specific content display.
590type FetchToolItem struct {
591 toolItem
592}
593
594func NewFetchToolItem(ctx ToolCallContext) *FetchToolItem {
595 return &FetchToolItem{
596 toolItem: newToolItem(ctx),
597 }
598}
599
600func (m *FetchToolItem) Render(width int) string {
601 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
602 return m.renderPending()
603 }
604
605 var params tools.FetchParams
606 unmarshalParams(m.ctx.Call.Input, ¶ms)
607
608 args := NewParamBuilder().
609 Main(params.URL).
610 KeyValue("format", params.Format).
611 KeyValue("timeout", formatTimeout(params.Timeout)).
612 Build()
613
614 header := renderToolHeader(&m.ctx, "Fetch", width, args...)
615
616 if result, done := renderEarlyState(&m.ctx, header, width); done {
617 return result
618 }
619
620 file := "fetch.md"
621 switch params.Format {
622 case "text":
623 file = "fetch.txt"
624 case "html":
625 file = "fetch.html"
626 }
627
628 body := renderCodeContent(file, m.ctx.Result.Content, 0, width-2, m.ctx.Styles, &m.toolItem)
629 return joinHeaderBody(header, body, m.ctx.Styles)
630}
631
632// AgenticFetchToolItem renders agentic URL fetching with nested tool calls.
633type AgenticFetchToolItem struct {
634 toolItem
635}
636
637func NewAgenticFetchToolItem(ctx ToolCallContext) *AgenticFetchToolItem {
638 return &AgenticFetchToolItem{
639 toolItem: newToolItem(ctx),
640 }
641}
642
643func (m *AgenticFetchToolItem) Render(width int) string {
644 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
645 return m.renderPending()
646 }
647
648 var params tools.AgenticFetchParams
649 unmarshalParams(m.ctx.Call.Input, ¶ms)
650
651 var args []string
652 if params.URL != "" {
653 args = NewParamBuilder().Main(params.URL).Build()
654 }
655
656 header := renderToolHeader(&m.ctx, "Agentic Fetch", width, args...)
657
658 // Render with nested tool calls tree
659 body := renderAgentBody(&m.ctx, params.Prompt, "Prompt", header, width)
660 return body
661}
662
663// WebFetchToolItem renders web page fetching.
664type WebFetchToolItem struct {
665 toolItem
666}
667
668func NewWebFetchToolItem(ctx ToolCallContext) *WebFetchToolItem {
669 return &WebFetchToolItem{
670 toolItem: newToolItem(ctx),
671 }
672}
673
674func (m *WebFetchToolItem) Render(width int) string {
675 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
676 return m.renderPending()
677 }
678
679 var params tools.WebFetchParams
680 unmarshalParams(m.ctx.Call.Input, ¶ms)
681
682 args := NewParamBuilder().Main(params.URL).Build()
683 header := renderToolHeader(&m.ctx, "Fetch", width, args...)
684
685 if result, done := renderEarlyState(&m.ctx, header, width); done {
686 return result
687 }
688
689 body := renderMarkdownContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
690 return joinHeaderBody(header, body, m.ctx.Styles)
691}
692
693// WebSearchToolItem renders web search results.
694type WebSearchToolItem struct {
695 toolItem
696}
697
698func NewWebSearchToolItem(ctx ToolCallContext) *WebSearchToolItem {
699 return &WebSearchToolItem{
700 toolItem: newToolItem(ctx),
701 }
702}
703
704func (m *WebSearchToolItem) Render(width int) string {
705 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
706 return m.renderPending()
707 }
708
709 var params tools.WebSearchParams
710 unmarshalParams(m.ctx.Call.Input, ¶ms)
711
712 args := NewParamBuilder().Main(params.Query).Build()
713 header := renderToolHeader(&m.ctx, "Search", width, args...)
714
715 if result, done := renderEarlyState(&m.ctx, header, width); done {
716 return result
717 }
718
719 body := renderMarkdownContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
720 return joinHeaderBody(header, body, m.ctx.Styles)
721}
722
723// DownloadToolItem renders file downloading.
724type DownloadToolItem struct {
725 toolItem
726}
727
728func NewDownloadToolItem(ctx ToolCallContext) *DownloadToolItem {
729 return &DownloadToolItem{
730 toolItem: newToolItem(ctx),
731 }
732}
733
734func (m *DownloadToolItem) Render(width int) string {
735 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
736 return m.renderPending()
737 }
738
739 var params tools.DownloadParams
740 unmarshalParams(m.ctx.Call.Input, ¶ms)
741
742 args := NewParamBuilder().
743 Main(params.URL).
744 KeyValue("file_path", fsext.PrettyPath(params.FilePath)).
745 KeyValue("timeout", formatTimeout(params.Timeout)).
746 Build()
747
748 header := renderToolHeader(&m.ctx, "Download", width, args...)
749
750 if result, done := renderEarlyState(&m.ctx, header, width); done {
751 return result
752 }
753
754 body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
755 return joinHeaderBody(header, body, m.ctx.Styles)
756}
757
758// -----------------------------------------------------------------------------
759// LSP Tools
760// -----------------------------------------------------------------------------
761
762// DiagnosticsToolItem renders project-wide diagnostic information.
763type DiagnosticsToolItem struct {
764 toolItem
765}
766
767func NewDiagnosticsToolItem(ctx ToolCallContext) *DiagnosticsToolItem {
768 return &DiagnosticsToolItem{
769 toolItem: newToolItem(ctx),
770 }
771}
772
773func (m *DiagnosticsToolItem) Render(width int) string {
774 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
775 return m.renderPending()
776 }
777
778 args := NewParamBuilder().Main("project").Build()
779 header := renderToolHeader(&m.ctx, "Diagnostics", width, args...)
780
781 if result, done := renderEarlyState(&m.ctx, header, width); done {
782 return result
783 }
784
785 body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
786 return joinHeaderBody(header, body, m.ctx.Styles)
787}
788
789// ReferencesToolItem renders LSP references search results.
790type ReferencesToolItem struct {
791 toolItem
792}
793
794func NewReferencesToolItem(ctx ToolCallContext) *ReferencesToolItem {
795 return &ReferencesToolItem{
796 toolItem: newToolItem(ctx),
797 }
798}
799
800func (m *ReferencesToolItem) Render(width int) string {
801 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
802 return m.renderPending()
803 }
804
805 var params tools.ReferencesParams
806 unmarshalParams(m.ctx.Call.Input, ¶ms)
807
808 args := NewParamBuilder().
809 Main(params.Symbol).
810 KeyValue("path", fsext.PrettyPath(params.Path)).
811 Build()
812
813 header := renderToolHeader(&m.ctx, "References", width, args...)
814
815 if result, done := renderEarlyState(&m.ctx, header, width); done {
816 return result
817 }
818
819 body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
820 return joinHeaderBody(header, body, m.ctx.Styles)
821}
822
823// -----------------------------------------------------------------------------
824// Misc Tools
825// -----------------------------------------------------------------------------
826
827// TodosToolItem renders todo list management.
828type TodosToolItem struct {
829 toolItem
830}
831
832func NewTodosToolItem(ctx ToolCallContext) *TodosToolItem {
833 return &TodosToolItem{
834 toolItem: newToolItem(ctx),
835 }
836}
837
838func (m *TodosToolItem) Render(width int) string {
839 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
840 return m.renderPending()
841 }
842
843 sty := m.ctx.Styles
844 var params tools.TodosParams
845 var meta tools.TodosResponseMetadata
846 var headerText string
847 var body string
848
849 // Parse params for pending state
850 if err := unmarshalParams(m.ctx.Call.Input, ¶ms); err == nil {
851 completedCount := 0
852 inProgressTask := ""
853 for _, todo := range params.Todos {
854 if todo.Status == "completed" {
855 completedCount++
856 }
857 if todo.Status == "in_progress" {
858 inProgressTask = cmp.Or(todo.ActiveForm, todo.Content)
859 }
860 }
861
862 // Default display from params
863 ratio := sty.Tool.JobAction.Render(fmt.Sprintf("%d/%d", completedCount, len(params.Todos)))
864 headerText = ratio
865 if inProgressTask != "" {
866 headerText = fmt.Sprintf("%s · %s", ratio, inProgressTask)
867 }
868
869 // If we have metadata, use it for richer display
870 if m.ctx.Result != nil && m.ctx.Result.Metadata != "" {
871 if err := unmarshalParams(m.ctx.Result.Metadata, &meta); err == nil {
872 headerText, body = m.formatTodosFromMeta(meta, width)
873 }
874 }
875 }
876
877 args := NewParamBuilder().Main(headerText).Build()
878 header := renderToolHeader(&m.ctx, "To-Do", width, args...)
879
880 if result, done := renderEarlyState(&m.ctx, header, width); done {
881 return result
882 }
883
884 if body == "" {
885 return header
886 }
887 return joinHeaderBody(header, body, m.ctx.Styles)
888}
889
890func (m *TodosToolItem) formatTodosFromMeta(meta tools.TodosResponseMetadata, width int) (string, string) {
891 sty := m.ctx.Styles
892 var headerText, body string
893
894 if meta.IsNew {
895 if meta.JustStarted != "" {
896 headerText = fmt.Sprintf("created %d todos, starting first", meta.Total)
897 } else {
898 headerText = fmt.Sprintf("created %d todos", meta.Total)
899 }
900 body = formatTodosList(meta.Todos, width, sty)
901 } else {
902 hasCompleted := len(meta.JustCompleted) > 0
903 hasStarted := meta.JustStarted != ""
904 allCompleted := meta.Completed == meta.Total
905
906 ratio := sty.Tool.JobAction.Render(fmt.Sprintf("%d/%d", meta.Completed, meta.Total))
907 if hasCompleted && hasStarted {
908 text := sty.Tool.JobDescription.Render(fmt.Sprintf(" · completed %d, starting next", len(meta.JustCompleted)))
909 headerText = ratio + text
910 } else if hasCompleted {
911 text := " · completed all"
912 if !allCompleted {
913 text = fmt.Sprintf(" · completed %d", len(meta.JustCompleted))
914 }
915 headerText = ratio + sty.Tool.JobDescription.Render(text)
916 } else if hasStarted {
917 headerText = ratio + sty.Tool.JobDescription.Render(" · starting task")
918 } else {
919 headerText = ratio
920 }
921
922 if allCompleted {
923 body = formatTodosList(meta.Todos, width, sty)
924 } else if meta.JustStarted != "" {
925 body = sty.Tool.IconSuccess.String() + " " + sty.Base.Render(meta.JustStarted)
926 }
927 }
928
929 return headerText, body
930}
931
932// AgentToolItem renders agent task execution with nested tool calls.
933type AgentToolItem struct {
934 toolItem
935}
936
937func NewAgentToolItem(ctx ToolCallContext) *AgentToolItem {
938 return &AgentToolItem{
939 toolItem: newToolItem(ctx),
940 }
941}
942
943func (m *AgentToolItem) Render(width int) string {
944 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
945 return m.renderPending()
946 }
947
948 var params agent.AgentParams
949 unmarshalParams(m.ctx.Call.Input, ¶ms)
950
951 header := renderToolHeader(&m.ctx, "Agent", width)
952 body := renderAgentBody(&m.ctx, params.Prompt, "Task", header, width)
953 return body
954}
955
956// renderAgentBody renders agent/agentic_fetch body with prompt tag and nested calls tree.
957func renderAgentBody(ctx *ToolCallContext, prompt, tagLabel, header string, width int) string {
958 sty := ctx.Styles
959
960 if ctx.Cancelled {
961 if result, done := renderEarlyState(ctx, header, width); done {
962 return result
963 }
964 }
965
966 // Build prompt tag
967 prompt = strings.ReplaceAll(prompt, "\n", " ")
968 taskTag := sty.Tool.AgentTaskTag.Render(tagLabel)
969 tagWidth := lipgloss.Width(taskTag)
970 remainingWidth := min(width-tagWidth-2, 120-tagWidth-2)
971 promptStyled := sty.Tool.AgentPrompt.Width(remainingWidth).Render(prompt)
972
973 headerWithPrompt := lipgloss.JoinVertical(
974 lipgloss.Left,
975 header,
976 "",
977 lipgloss.JoinHorizontal(lipgloss.Left, taskTag, " ", promptStyled),
978 )
979
980 // Build tree with nested tool calls
981 childTools := tree.Root(headerWithPrompt)
982 for _, nestedCtx := range ctx.NestedCalls {
983 nestedCtx.IsNested = true
984 nestedItem := NewToolItem(nestedCtx)
985 childTools.Child(nestedItem.Render(remainingWidth))
986 }
987
988 parts := []string{
989 childTools.Enumerator(roundedEnumerator(2, tagWidth-5)).String(),
990 }
991
992 if !ctx.HasResult() {
993 parts = append(parts, "", sty.Tool.StateWaiting.Render("Working..."))
994 }
995
996 treeOutput := lipgloss.JoinVertical(lipgloss.Left, parts...)
997
998 if !ctx.HasResult() {
999 return treeOutput
1000 }
1001
1002 body := renderMarkdownContent(ctx.Result.Content, width-2, sty, nil)
1003 return joinHeaderBody(treeOutput, body, sty)
1004}
1005
1006// roundedEnumerator creates a tree enumerator with rounded connectors.
1007func roundedEnumerator(lPadding, lineWidth int) tree.Enumerator {
1008 if lineWidth == 0 {
1009 lineWidth = 2
1010 }
1011 if lPadding == 0 {
1012 lPadding = 1
1013 }
1014 return func(children tree.Children, index int) string {
1015 line := strings.Repeat("─", lineWidth)
1016 padding := strings.Repeat(" ", lPadding)
1017 if children.Length()-1 == index {
1018 return padding + "╰" + line
1019 }
1020 return padding + "├" + line
1021 }
1022}
1023
1024// GenericToolItem renders unknown tool types with basic parameter display.
1025type GenericToolItem struct {
1026 toolItem
1027}
1028
1029func NewGenericToolItem(ctx ToolCallContext) *GenericToolItem {
1030 return &GenericToolItem{
1031 toolItem: newToolItem(ctx),
1032 }
1033}
1034
1035func (m *GenericToolItem) Render(width int) string {
1036 if !m.ctx.Call.Finished && !m.ctx.Cancelled {
1037 return m.renderPending()
1038 }
1039
1040 name := prettifyToolName(m.ctx.Call.Name)
1041
1042 // Handle media content
1043 if m.ctx.Result != nil && m.ctx.Result.Data != "" {
1044 if strings.HasPrefix(m.ctx.Result.MIMEType, "image/") {
1045 args := NewParamBuilder().Main(m.toolItem.ctx.Call.Input).Build()
1046 header := renderToolHeader(&m.ctx, name, width, args...)
1047 body := renderImageContent(m.ctx.Result.Data, m.ctx.Result.MIMEType, m.ctx.Result.Content, m.ctx.Styles)
1048 return joinHeaderBody(header, body, m.ctx.Styles)
1049 }
1050 args := NewParamBuilder().Main(m.toolItem.ctx.Call.Input).Build()
1051 header := renderToolHeader(&m.ctx, name, width, args...)
1052 body := renderMediaContent(m.ctx.Result.MIMEType, m.ctx.Result.Content, m.ctx.Styles)
1053 return joinHeaderBody(header, body, m.ctx.Styles)
1054 }
1055
1056 args := NewParamBuilder().Main(m.toolItem.ctx.Call.Input).Build()
1057 header := renderToolHeader(&m.ctx, name, width, args...)
1058
1059 if result, done := renderEarlyState(&m.ctx, header, width); done {
1060 return result
1061 }
1062
1063 if m.ctx.Result == nil || m.ctx.Result.Content == "" {
1064 return header
1065 }
1066
1067 body := renderPlainContent(m.ctx.Result.Content, width-2, m.ctx.Styles, &m.toolItem)
1068 return joinHeaderBody(header, body, m.ctx.Styles)
1069}
1070
1071// -----------------------------------------------------------------------------
1072// Helper Functions
1073// -----------------------------------------------------------------------------
1074
1075// prettifyToolName converts tool names to display-friendly format.
1076func prettifyToolName(name string) string {
1077 switch name {
1078 case agent.AgentToolName:
1079 return "Agent"
1080 case tools.BashToolName:
1081 return "Bash"
1082 case tools.JobOutputToolName:
1083 return "Job: Output"
1084 case tools.JobKillToolName:
1085 return "Job: Kill"
1086 case tools.DownloadToolName:
1087 return "Download"
1088 case tools.EditToolName:
1089 return "Edit"
1090 case tools.MultiEditToolName:
1091 return "Multi-Edit"
1092 case tools.FetchToolName:
1093 return "Fetch"
1094 case tools.AgenticFetchToolName:
1095 return "Agentic Fetch"
1096 case tools.WebFetchToolName:
1097 return "Fetch"
1098 case tools.WebSearchToolName:
1099 return "Search"
1100 case tools.GlobToolName:
1101 return "Glob"
1102 case tools.GrepToolName:
1103 return "Grep"
1104 case tools.LSToolName:
1105 return "List"
1106 case tools.SourcegraphToolName:
1107 return "Sourcegraph"
1108 case tools.TodosToolName:
1109 return "To-Do"
1110 case tools.ViewToolName:
1111 return "View"
1112 case tools.WriteToolName:
1113 return "Write"
1114 case tools.DiagnosticsToolName:
1115 return "Diagnostics"
1116 case tools.ReferencesToolName:
1117 return "References"
1118 default:
1119 // Handle MCP tools and others
1120 name = strings.TrimPrefix(name, "mcp_")
1121 if name == "" {
1122 return "Tool"
1123 }
1124 return strings.ToUpper(name[:1]) + name[1:]
1125 }
1126}
1127
1128// formatTimeout converts timeout seconds to duration string.
1129func formatTimeout(timeout int) string {
1130 if timeout == 0 {
1131 return ""
1132 }
1133 return (time.Duration(timeout) * time.Second).String()
1134}
1135
1136// truncateText truncates text to fit within width with ellipsis.
1137func truncateText(s string, width int) string {
1138 if lipgloss.Width(s) <= width {
1139 return s
1140 }
1141 for i := len(s) - 1; i >= 0; i-- {
1142 truncated := s[:i] + "…"
1143 if lipgloss.Width(truncated) <= width {
1144 return truncated
1145 }
1146 }
1147 return "…"
1148}
1149
1150// Update implements list.Updatable.
1151func (m *JobOutputToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1152 cmd, changed := m.updateAnimation(msg)
1153 if changed {
1154 return m, cmd
1155 }
1156 return m, nil
1157}
1158
1159// Update implements list.Updatable.
1160func (m *JobKillToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1161 cmd, changed := m.updateAnimation(msg)
1162 if changed {
1163 return m, cmd
1164 }
1165 return m, nil
1166}
1167
1168// Update implements list.Updatable.
1169func (m *ViewToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1170 cmd, changed := m.updateAnimation(msg)
1171 if changed {
1172 return m, cmd
1173 }
1174 return m, nil
1175}
1176
1177// Update implements list.Updatable.
1178func (m *EditToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1179 cmd, changed := m.updateAnimation(msg)
1180 if changed {
1181 return m, cmd
1182 }
1183 return m, nil
1184}
1185
1186// Update implements list.Updatable.
1187func (m *MultiEditToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1188 cmd, changed := m.updateAnimation(msg)
1189 if changed {
1190 return m, cmd
1191 }
1192 return m, nil
1193}
1194
1195// Update implements list.Updatable.
1196func (m *WriteToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1197 cmd, changed := m.updateAnimation(msg)
1198 if changed {
1199 return m, cmd
1200 }
1201 return m, nil
1202}
1203
1204// Update implements list.Updatable.
1205func (m *GlobToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1206 cmd, changed := m.updateAnimation(msg)
1207 if changed {
1208 return m, cmd
1209 }
1210 return m, nil
1211}
1212
1213// Update implements list.Updatable.
1214func (m *GrepToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1215 cmd, changed := m.updateAnimation(msg)
1216 if changed {
1217 return m, cmd
1218 }
1219 return m, nil
1220}
1221
1222// Update implements list.Updatable.
1223func (m *LSToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1224 cmd, changed := m.updateAnimation(msg)
1225 if changed {
1226 return m, cmd
1227 }
1228 return m, nil
1229}
1230
1231// Update implements list.Updatable.
1232func (m *SourcegraphToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1233 cmd, changed := m.updateAnimation(msg)
1234 if changed {
1235 return m, cmd
1236 }
1237 return m, nil
1238}
1239
1240// Update implements list.Updatable.
1241func (m *FetchToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1242 cmd, changed := m.updateAnimation(msg)
1243 if changed {
1244 return m, cmd
1245 }
1246 return m, nil
1247}
1248
1249// Update implements list.Updatable.
1250func (m *AgenticFetchToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1251 cmd, changed := m.updateAnimation(msg)
1252 if changed {
1253 return m, cmd
1254 }
1255 return m, nil
1256}
1257
1258// Update implements list.Updatable.
1259func (m *WebFetchToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1260 cmd, changed := m.updateAnimation(msg)
1261 if changed {
1262 return m, cmd
1263 }
1264 return m, nil
1265}
1266
1267// Update implements list.Updatable.
1268func (m *WebSearchToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1269 cmd, changed := m.updateAnimation(msg)
1270 if changed {
1271 return m, cmd
1272 }
1273 return m, nil
1274}
1275
1276// Update implements list.Updatable.
1277func (m *DownloadToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1278 cmd, changed := m.updateAnimation(msg)
1279 if changed {
1280 return m, cmd
1281 }
1282 return m, nil
1283}
1284
1285// Update implements list.Updatable.
1286func (m *DiagnosticsToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1287 cmd, changed := m.updateAnimation(msg)
1288 if changed {
1289 return m, cmd
1290 }
1291 return m, nil
1292}
1293
1294// Update implements list.Updatable.
1295func (m *ReferencesToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1296 cmd, changed := m.updateAnimation(msg)
1297 if changed {
1298 return m, cmd
1299 }
1300 return m, nil
1301}
1302
1303// Update implements list.Updatable.
1304func (m *TodosToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1305 cmd, changed := m.updateAnimation(msg)
1306 if changed {
1307 return m, cmd
1308 }
1309 return m, nil
1310}
1311
1312// Update implements list.Updatable.
1313func (m *AgentToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1314 cmd, changed := m.updateAnimation(msg)
1315 if changed {
1316 return m, cmd
1317 }
1318 return m, nil
1319}
1320
1321// Update implements list.Updatable.
1322func (m *GenericToolItem) Update(msg tea.Msg) (list.Item, tea.Cmd) {
1323 cmd, changed := m.updateAnimation(msg)
1324 if changed {
1325 return m, cmd
1326 }
1327 return m, nil
1328}