diff --git a/internal/acp/sink.go b/internal/acp/sink.go index fd739b894ddadbe6d67c941c7fdd11024efe087b..9a9b71d9e8f9e3f4ca0d6c0a54e2ee3b2b95f59a 100644 --- a/internal/acp/sink.go +++ b/internal/acp/sink.go @@ -2,6 +2,7 @@ package acp import ( "context" + "encoding/json" "log/slog" "github.com/charmbracelet/crush/internal/message" @@ -216,11 +217,20 @@ func (s *Sink) translateReasoning(msgID string, reasoning message.ReasoningConte func (s *Sink) translateToolCall(tc message.ToolCall) *acp.SessionUpdate { if !tc.Finished { - update := acp.StartToolCall( - acp.ToolCallId(tc.ID), - tc.Name, + opts := []acp.ToolCallStartOpt{ acp.WithStartStatus(acp.ToolCallStatusPending), - ) + acp.WithStartKind(toolKind(tc.Name)), + } + + // Parse input to extract path and raw input. + if input := parseToolInput(tc.Input); input != nil { + if input.Path != "" { + opts = append(opts, acp.WithStartLocations([]acp.ToolCallLocation{{Path: input.Path}})) + } + opts = append(opts, acp.WithStartRawInput(input.Raw)) + } + + update := acp.StartToolCall(acp.ToolCallId(tc.ID), tc.Name, opts...) return &update } @@ -231,6 +241,53 @@ func (s *Sink) translateToolCall(tc message.ToolCall) *acp.SessionUpdate { return &update } +// toolInput holds parsed tool call input. +type toolInput struct { + Path string + Raw map[string]any +} + +// parseToolInput extracts path and raw input from JSON tool input. +func parseToolInput(input string) *toolInput { + if input == "" { + return nil + } + + var raw map[string]any + if err := json.Unmarshal([]byte(input), &raw); err != nil { + return nil + } + + ti := &toolInput{Raw: raw} + + // Extract path from common field names. + if path, ok := raw["file_path"].(string); ok { + ti.Path = path + } else if path, ok := raw["path"].(string); ok { + ti.Path = path + } + + return ti +} + +// toolKind maps Crush tool names to ACP tool kinds. +func toolKind(name string) acp.ToolKind { + switch name { + case "view", "ls", "job_output", "lsp_diagnostics": + return acp.ToolKindRead + case "edit", "multiedit", "write": + return acp.ToolKindEdit + case "bash", "job_kill": + return acp.ToolKindExecute + case "grep", "glob", "lsp_references", "sourcegraph", "web_search": + return acp.ToolKindSearch + case "fetch", "agentic_fetch", "web_fetch", "download": + return acp.ToolKindFetch + default: + return acp.ToolKindOther + } +} + func (s *Sink) translateToolResult(tr message.ToolResult) *acp.SessionUpdate { status := acp.ToolCallStatusCompleted if tr.IsError {