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