From 759f2d20658e358f47f1d319f6f2209509b4cb1b Mon Sep 17 00:00:00 2001 From: Amolith Date: Sat, 3 Jan 2026 18:19:29 -0700 Subject: [PATCH] feat(acp): map tool names to ACP tool kinds Maps Crush tool names to semantic ACP ToolKinds (read, edit, execute, search, fetch, other) and extracts file paths from tool input JSON for richer client rendering. Assisted-by: Claude Opus 4.5 via Crush --- internal/acp/sink.go | 65 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 4 deletions(-) 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 {