Detailed changes
@@ -110,16 +110,27 @@ func (s *Sink) HandlePermission(req permission.PermissionRequest, permissions pe
slog.Debug("ACP permission request", "tool", req.ToolName, "action", req.Action)
+ // Build the tool call for the permission request.
+ toolCall := acp.RequestPermissionToolCall{
+ ToolCallId: acp.ToolCallId(req.ToolCallID),
+ Title: acp.Ptr(req.Description),
+ Kind: acp.Ptr(acp.ToolKindEdit),
+ Status: acp.Ptr(acp.ToolCallStatusPending),
+ Locations: []acp.ToolCallLocation{{Path: req.Path}},
+ RawInput: req.Params,
+ }
+
+ // For edit tools, include diff content so the client can show the proposed
+ // changes.
+ if meta := extractEditParams(req.Params); meta != nil && meta.FilePath != "" {
+ toolCall.Content = []acp.ToolCallContent{
+ acp.ToolDiffContent(meta.FilePath, meta.NewContent, meta.OldContent),
+ }
+ }
+
resp, err := s.conn.RequestPermission(s.ctx, acp.RequestPermissionRequest{
SessionId: acp.SessionId(s.sessionID),
- ToolCall: acp.RequestPermissionToolCall{
- ToolCallId: acp.ToolCallId(req.ToolCallID),
- Title: acp.Ptr(req.Description),
- Kind: acp.Ptr(acp.ToolKindEdit),
- Status: acp.Ptr(acp.ToolCallStatusPending),
- Locations: []acp.ToolCallLocation{{Path: req.Path}},
- RawInput: req.Params,
- },
+ ToolCall: toolCall,
Options: []acp.PermissionOption{
{Kind: acp.PermissionOptionKindAllowOnce, Name: "Allow", OptionId: "allow"},
{Kind: acp.PermissionOptionKindAllowAlways, Name: "Allow always", OptionId: "allow_always"},
@@ -149,6 +160,33 @@ func (s *Sink) HandlePermission(req permission.PermissionRequest, permissions pe
}
}
+// editParams holds fields needed for diff content in permission requests.
+type editParams struct {
+ FilePath string `json:"file_path"`
+ OldContent string `json:"old_content"`
+ NewContent string `json:"new_content"`
+}
+
+// extractEditParams attempts to extract edit parameters from permission params.
+func extractEditParams(params any) *editParams {
+ if params == nil {
+ return nil
+ }
+
+ // Try JSON round-trip to extract fields.
+ data, err := json.Marshal(params)
+ if err != nil {
+ return nil
+ }
+
+ var ep editParams
+ if err := json.Unmarshal(data, &ep); err != nil {
+ return nil
+ }
+
+ return &ep
+}
+
// translatePart converts a message part to an ACP session update.
func (s *Sink) translatePart(msgID string, role message.MessageRole, part message.ContentPart) *acp.SessionUpdate {
switch p := part.(type) {
@@ -288,18 +326,37 @@ func toolKind(name string) acp.ToolKind {
}
}
+// diffMetadata holds fields common to edit tool response metadata.
+type diffMetadata struct {
+ FilePath string `json:"file_path"`
+ OldContent string `json:"old_content"`
+ NewContent string `json:"new_content"`
+}
+
func (s *Sink) translateToolResult(tr message.ToolResult) *acp.SessionUpdate {
status := acp.ToolCallStatusCompleted
if tr.IsError {
status = acp.ToolCallStatusFailed
}
+ // For edit tools with metadata, emit diff content.
+ content := []acp.ToolCallContent{acp.ToolContent(acp.TextBlock(tr.Content))}
+ if !tr.IsError && tr.Metadata != "" {
+ switch tr.Name {
+ case "edit", "multiedit", "write":
+ var meta diffMetadata
+ if err := json.Unmarshal([]byte(tr.Metadata), &meta); err == nil && meta.FilePath != "" {
+ content = []acp.ToolCallContent{
+ acp.ToolDiffContent(meta.FilePath, meta.NewContent, meta.OldContent),
+ }
+ }
+ }
+ }
+
update := acp.UpdateToolCall(
acp.ToolCallId(tr.ToolCallID),
acp.WithUpdateStatus(status),
- acp.WithUpdateContent([]acp.ToolCallContent{
- acp.ToolContent(acp.TextBlock(tr.Content)),
- }),
+ acp.WithUpdateContent(content),
)
return &update
}
@@ -35,10 +35,11 @@ type EditPermissionsParams struct {
}
type EditResponseMetadata struct {
- Additions int `json:"additions"`
- Removals int `json:"removals"`
+ FilePath string `json:"file_path"`
OldContent string `json:"old_content,omitempty"`
NewContent string `json:"new_content,omitempty"`
+ Additions int `json:"additions"`
+ Removals int `json:"removals"`
}
const EditToolName = "edit"
@@ -165,6 +166,7 @@ func createNewFile(edit editContext, filePath, content string, call fantasy.Tool
return fantasy.WithResponseMetadata(
fantasy.NewTextResponse("File created: "+filePath),
EditResponseMetadata{
+ FilePath: filePath,
OldContent: "",
NewContent: content,
Additions: additions,
@@ -298,6 +300,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
return fantasy.WithResponseMetadata(
fantasy.NewTextResponse("Content deleted from file: "+filePath),
EditResponseMetadata{
+ FilePath: filePath,
OldContent: oldContent,
NewContent: newContent,
Additions: additions,
@@ -433,6 +436,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
return fantasy.WithResponseMetadata(
fantasy.NewTextResponse("Content replaced in file: "+filePath),
EditResponseMetadata{
+ FilePath: filePath,
OldContent: oldContent,
NewContent: newContent,
Additions: additions,
@@ -44,10 +44,11 @@ type FailedEdit struct {
}
type MultiEditResponseMetadata struct {
- Additions int `json:"additions"`
- Removals int `json:"removals"`
+ FilePath string `json:"file_path"`
OldContent string `json:"old_content,omitempty"`
NewContent string `json:"new_content,omitempty"`
+ Additions int `json:"additions"`
+ Removals int `json:"removals"`
EditsApplied int `json:"edits_applied"`
EditsFailed []FailedEdit `json:"edits_failed,omitempty"`
}
@@ -219,6 +220,7 @@ func processMultiEditWithCreation(edit editContext, params MultiEditParams, call
return fantasy.WithResponseMetadata(
fantasy.NewTextResponse(message),
MultiEditResponseMetadata{
+ FilePath: params.FilePath,
OldContent: "",
NewContent: currentContent,
Additions: additions,
@@ -289,6 +291,7 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
return fantasy.WithResponseMetadata(
fantasy.NewTextErrorResponse(fmt.Sprintf("no changes made - all %d edit(s) failed", len(failedEdits))),
MultiEditResponseMetadata{
+ FilePath: params.FilePath,
EditsApplied: 0,
EditsFailed: failedEdits,
},
@@ -375,6 +378,7 @@ func processMultiEditExistingFile(edit editContext, params MultiEditParams, call
return fantasy.WithResponseMetadata(
fantasy.NewTextResponse(message),
MultiEditResponseMetadata{
+ FilePath: params.FilePath,
OldContent: oldContent,
NewContent: currentContent,
Additions: additions,
@@ -43,9 +43,12 @@ type writeTool struct {
}
type WriteResponseMetadata struct {
- Diff string `json:"diff"`
- Additions int `json:"additions"`
- Removals int `json:"removals"`
+ FilePath string `json:"file_path"`
+ OldContent string `json:"old_content,omitempty"`
+ NewContent string `json:"new_content,omitempty"`
+ Diff string `json:"diff"`
+ Additions int `json:"additions"`
+ Removals int `json:"removals"`
}
const WriteToolName = "write"
@@ -166,9 +169,12 @@ func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permis
result += getDiagnostics(filePath, lspClients)
return fantasy.WithResponseMetadata(fantasy.NewTextResponse(result),
WriteResponseMetadata{
- Diff: diff,
- Additions: additions,
- Removals: removals,
+ FilePath: filePath,
+ OldContent: oldContent,
+ NewContent: params.Content,
+ Diff: diff,
+ Additions: additions,
+ Removals: removals,
},
), nil
})