sink_tools.go

  1package acp
  2
  3import (
  4	"encoding/json"
  5
  6	"github.com/charmbracelet/crush/internal/message"
  7	"github.com/coder/acp-go-sdk"
  8)
  9
 10func (s *Sink) translateToolCall(tc message.ToolCall) *acp.SessionUpdate {
 11	if !tc.Finished {
 12		opts := []acp.ToolCallStartOpt{
 13			acp.WithStartStatus(acp.ToolCallStatusPending),
 14			acp.WithStartKind(toolKind(tc.Name)),
 15		}
 16
 17		// Parse input to extract path, title, and raw input.
 18		title := tc.Name
 19		if input := parseToolInput(tc.Input); input != nil {
 20			if input.Path != "" {
 21				opts = append(opts, acp.WithStartLocations([]acp.ToolCallLocation{{Path: input.Path}}))
 22			}
 23			if input.Title != "" {
 24				title = input.Title
 25			}
 26			opts = append(opts, acp.WithStartRawInput(input.Raw))
 27		}
 28
 29		update := acp.StartToolCall(acp.ToolCallId(tc.ID), title, opts...)
 30		return &update
 31	}
 32
 33	// Tool finished streaming - update with title and input now available.
 34	opts := []acp.ToolCallUpdateOpt{
 35		acp.WithUpdateStatus(acp.ToolCallStatusInProgress),
 36	}
 37	if input := parseToolInput(tc.Input); input != nil {
 38		if input.Title != "" {
 39			opts = append(opts, acp.WithUpdateTitle(input.Title))
 40		}
 41		if input.Path != "" {
 42			opts = append(opts, acp.WithUpdateLocations([]acp.ToolCallLocation{{Path: input.Path}}))
 43		}
 44		opts = append(opts, acp.WithUpdateRawInput(input.Raw))
 45	}
 46
 47	update := acp.UpdateToolCall(acp.ToolCallId(tc.ID), opts...)
 48	return &update
 49}
 50
 51// toolInput holds parsed tool call input.
 52type toolInput struct {
 53	Path  string
 54	Title string
 55	Raw   map[string]any
 56}
 57
 58// parseToolInput extracts path and raw input from JSON tool input.
 59func parseToolInput(input string) *toolInput {
 60	if input == "" {
 61		return nil
 62	}
 63
 64	var raw map[string]any
 65	if err := json.Unmarshal([]byte(input), &raw); err != nil {
 66		return nil
 67	}
 68
 69	ti := &toolInput{Raw: raw}
 70
 71	// Extract path from common field names.
 72	if path, ok := raw["file_path"].(string); ok {
 73		ti.Path = path
 74	} else if path, ok := raw["path"].(string); ok {
 75		ti.Path = path
 76	}
 77
 78	// Extract title/description for display.
 79	if desc, ok := raw["description"].(string); ok {
 80		ti.Title = desc
 81	}
 82
 83	return ti
 84}
 85
 86// toolKind maps Crush tool names to ACP tool kinds.
 87func toolKind(name string) acp.ToolKind {
 88	switch name {
 89	case "view", "ls", "job_output", "lsp_diagnostics":
 90		return acp.ToolKindRead
 91	case "edit", "multiedit", "write":
 92		return acp.ToolKindEdit
 93	case "bash", "job_kill":
 94		return acp.ToolKindExecute
 95	case "grep", "glob", "lsp_references", "sourcegraph", "web_search":
 96		return acp.ToolKindSearch
 97	case "fetch", "agentic_fetch", "web_fetch", "download":
 98		return acp.ToolKindFetch
 99	default:
100		return acp.ToolKindOther
101	}
102}
103
104// diffMetadata holds fields common to edit tool response metadata.
105type diffMetadata struct {
106	FilePath   string `json:"file_path"`
107	OldContent string `json:"old_content"`
108	NewContent string `json:"new_content"`
109}
110
111func (s *Sink) translateToolResult(tr message.ToolResult) *acp.SessionUpdate {
112	status := acp.ToolCallStatusCompleted
113	if tr.IsError {
114		status = acp.ToolCallStatusFailed
115	}
116
117	// For edit tools with metadata, emit diff content.
118	content := []acp.ToolCallContent{acp.ToolContent(acp.TextBlock(tr.Content))}
119	var locations []acp.ToolCallLocation
120
121	if !tr.IsError && tr.Metadata != "" {
122		switch tr.Name {
123		case "edit", "multiedit", "write":
124			var meta diffMetadata
125			if err := json.Unmarshal([]byte(tr.Metadata), &meta); err == nil && meta.FilePath != "" {
126				content = []acp.ToolCallContent{
127					acp.ToolDiffContent(meta.FilePath, meta.NewContent, meta.OldContent),
128				}
129			}
130		case "view":
131			var meta struct {
132				FilePath string `json:"file_path"`
133			}
134			if err := json.Unmarshal([]byte(tr.Metadata), &meta); err == nil && meta.FilePath != "" {
135				locations = []acp.ToolCallLocation{{Path: meta.FilePath}}
136			}
137		case "ls":
138			var meta struct {
139				Path string `json:"path"`
140			}
141			if err := json.Unmarshal([]byte(tr.Metadata), &meta); err == nil && meta.Path != "" {
142				locations = []acp.ToolCallLocation{{Path: meta.Path}}
143			}
144		}
145	}
146
147	opts := []acp.ToolCallUpdateOpt{
148		acp.WithUpdateStatus(status),
149		acp.WithUpdateContent(content),
150	}
151	if len(locations) > 0 {
152		opts = append(opts, acp.WithUpdateLocations(locations))
153	}
154
155	update := acp.UpdateToolCall(acp.ToolCallId(tr.ToolCallID), opts...)
156	return &update
157}