tool_items.go

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