tool_items.go

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