on main help sign in
fix: remove broken patch tool
commit382b7f89cc32be33fda2b5c06c9dcb017fa26278 authorKujtim Hoxha <kujtimii.h@gmail.com> dateSat 14 Jun 2025, 13:33 (1 year ago) parenta092ca3
6 files changed+1−441
internal/exp/diffview/diffview.go+1−6 internal/llm/agent/tools.go−1 internal/llm/tools/patch.go−372 internal/tui/components/chat/messages/renderer.go−35 internal/tui/components/dialogs/permissions/permissions.go−16 internal/tui/tui.go−11
internal/exp/diffview/diffview.go+1−6
@@ -3,7 +3,6 @@ package diffview
33import (
44 "fmt"
55 "image/color"
6− "os"
76 "strconv"
87 "strings"
98
@@ -70,11 +69,7 @@ func New() *DiffView {
7069 lineNumbers: true,
7170 tabWidth: 8,
7271 }
73− if lipgloss.HasDarkBackground(os.Stdin, os.Stdout) {
74− dv.style = DefaultDarkStyle
75− } else {
76− dv.style = DefaultLightStyle
77− }
72+ dv.style = DefaultDarkStyle
7873 return dv
7974}
8075
internal/llm/agent/tools.go−1
@@ -33,7 +33,6 @@ func CoderAgentTools(
3333 tools.NewLsTool(),
3434 tools.NewSourcegraphTool(),
3535 tools.NewViewTool(lspClients),
36− tools.NewPatchTool(lspClients, permissions, history),
3736 tools.NewWriteTool(lspClients, permissions, history),
3837 NewAgentTool(sessions, messages, lspClients),
3938 }, otherTools...,
internal/llm/tools/patch.godeleted−372
@@ -1,372 +0,0 @@
1−package tools
2−
3−import (
4− "context"
5− "encoding/json"
6− "fmt"
7− "os"
8− "path/filepath"
9− "time"
10−
11− "github.com/charmbracelet/crush/internal/config"
12− "github.com/charmbracelet/crush/internal/diff"
13− "github.com/charmbracelet/crush/internal/history"
14− "github.com/charmbracelet/crush/internal/logging"
15− "github.com/charmbracelet/crush/internal/lsp"
16− "github.com/charmbracelet/crush/internal/permission"
17−)
18−
19−type PatchParams struct {
20− PatchText string `json:"patch_text"`
21−}
22−
23−type PatchResponseMetadata struct {
24− FilesChanged []string `json:"files_changed"`
25− Additions int `json:"additions"`
26− Removals int `json:"removals"`
27−}
28−
29−type patchTool struct {
30− lspClients map[string]*lsp.Client
31− permissions permission.Service
32− files history.Service
33−}
34−
35−const (
36− PatchToolName = "patch"
37− patchDescription = `Applies a patch to multiple files in one operation. This tool is useful for making coordinated changes across multiple files.
38−
39−The patch text must follow this format:
40−*** Begin Patch
41−*** Update File: /path/to/file
42−@@ Context line (unique within the file)
43− Line to keep
44−-Line to remove
45−+Line to add
46− Line to keep
47−*** Add File: /path/to/new/file
48−+Content of the new file
49−+More content
50−*** Delete File: /path/to/file/to/delete
51−*** End Patch
52−
53−Before using this tool:
54−1. Use the FileRead tool to understand the files' contents and context
55−2. Verify all file paths are correct (use the LS tool)
56−
57−CRITICAL REQUIREMENTS FOR USING THIS TOOL:
58−
59−1. UNIQUENESS: Context lines MUST uniquely identify the specific sections you want to change
60−2. PRECISION: All whitespace, indentation, and surrounding code must match exactly
61−3. VALIDATION: Ensure edits result in idiomatic, correct code
62−4. PATHS: Always use absolute file paths (starting with /)
63−
64−The tool will apply all changes in a single atomic operation.`
65−)
66−
67−func NewPatchTool(lspClients map[string]*lsp.Client, permissions permission.Service, files history.Service) BaseTool {
68− return &patchTool{
69− lspClients: lspClients,
70− permissions: permissions,
71− files: files,
72− }
73−}
74−
75−func (p *patchTool) Info() ToolInfo {
76− return ToolInfo{
77− Name: PatchToolName,
78− Description: patchDescription,
79− Parameters: map[string]any{
80− "patch_text": map[string]any{
81− "type": "string",
82− "description": "The full patch text that describes all changes to be made",
83− },
84− },
85− Required: []string{"patch_text"},
86− }
87−}
88−
89−func (p *patchTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error) {
90− var params PatchParams
91− if err := json.Unmarshal([]byte(call.Input), &params); err != nil {
92− return NewTextErrorResponse("invalid parameters"), nil
93− }
94−
95− if params.PatchText == "" {
96− return NewTextErrorResponse("patch_text is required"), nil
97− }
98−
99− // Identify all files needed for the patch and verify they've been read
100− filesToRead := diff.IdentifyFilesNeeded(params.PatchText)
101− for _, filePath := range filesToRead {
102− absPath := filePath
103− if !filepath.IsAbs(absPath) {
104− wd := config.WorkingDirectory()
105− absPath = filepath.Join(wd, absPath)
106− }
107−
108− if getLastReadTime(absPath).IsZero() {
109− return NewTextErrorResponse(fmt.Sprintf("you must read the file %s before patching it. Use the FileRead tool first", filePath)), nil
110− }
111−
112− fileInfo, err := os.Stat(absPath)
113− if err != nil {
114− if os.IsNotExist(err) {
115− return NewTextErrorResponse(fmt.Sprintf("file not found: %s", absPath)), nil
116− }
117− return ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
118− }
119−
120− if fileInfo.IsDir() {
121− return NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", absPath)), nil
122− }
123−
124− modTime := fileInfo.ModTime()
125− lastRead := getLastReadTime(absPath)
126− if modTime.After(lastRead) {
127− return NewTextErrorResponse(
128− fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
129− absPath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
130− )), nil
131− }
132− }
133−
134− // Check for new files to ensure they don't already exist
135− filesToAdd := diff.IdentifyFilesAdded(params.PatchText)
136− for _, filePath := range filesToAdd {
137− absPath := filePath
138− if !filepath.IsAbs(absPath) {
139− wd := config.WorkingDirectory()
140− absPath = filepath.Join(wd, absPath)
141− }
142−
143− _, err := os.Stat(absPath)
144− if err == nil {
145− return NewTextErrorResponse(fmt.Sprintf("file already exists and cannot be added: %s", absPath)), nil
146− } else if !os.IsNotExist(err) {
147− return ToolResponse{}, fmt.Errorf("failed to check file: %w", err)
148− }
149− }
150−
151− // Load all required files
152− currentFiles := make(map[string]string)
153− for _, filePath := range filesToRead {
154− absPath := filePath
155− if !filepath.IsAbs(absPath) {
156− wd := config.WorkingDirectory()
157− absPath = filepath.Join(wd, absPath)
158− }
159−
160− content, err := os.ReadFile(absPath)
161− if err != nil {
162− return ToolResponse{}, fmt.Errorf("failed to read file %s: %w", absPath, err)
163− }
164− currentFiles[filePath] = string(content)
165− }
166−
167− // Process the patch
168− patch, fuzz, err := diff.TextToPatch(params.PatchText, currentFiles)
169− if err != nil {
170− return NewTextErrorResponse(fmt.Sprintf("failed to parse patch: %s", err)), nil
171− }
172−
173− if fuzz > 3 {
174− return NewTextErrorResponse(fmt.Sprintf("patch contains fuzzy matches (fuzz level: %d). Please make your context lines more precise", fuzz)), nil
175− }
176−
177− // Convert patch to commit
178− commit, err := diff.PatchToCommit(patch, currentFiles)
179− if err != nil {
180− return NewTextErrorResponse(fmt.Sprintf("failed to create commit from patch: %s", err)), nil
181− }
182−
183− // Get session ID and message ID
184− sessionID, messageID := GetContextValues(ctx)
185− if sessionID == "" || messageID == "" {
186− return ToolResponse{}, fmt.Errorf("session ID and message ID are required for creating a patch")
187− }
188−
189− // Request permission for all changes
190− for path, change := range commit.Changes {
191− switch change.Type {
192− case diff.ActionAdd:
193− dir := filepath.Dir(path)
194− patchDiff, _, _ := diff.GenerateDiff("", *change.NewContent, path)
195− p := p.permissions.Request(
196− permission.CreatePermissionRequest{
197− SessionID: sessionID,
198− Path: dir,
199− ToolName: PatchToolName,
200− Action: "create",
201− Description: fmt.Sprintf("Create file %s", path),
202− Params: EditPermissionsParams{
203− FilePath: path,
204− Diff: patchDiff,
205− },
206− },
207− )
208− if !p {
209− return ToolResponse{}, permission.ErrorPermissionDenied
210− }
211− case diff.ActionUpdate:
212− currentContent := ""
213− if change.OldContent != nil {
214− currentContent = *change.OldContent
215− }
216− newContent := ""
217− if change.NewContent != nil {
218− newContent = *change.NewContent
219− }
220− patchDiff, _, _ := diff.GenerateDiff(currentContent, newContent, path)
221− dir := filepath.Dir(path)
222− p := p.permissions.Request(
223− permission.CreatePermissionRequest{
224− SessionID: sessionID,
225− Path: dir,
226− ToolName: PatchToolName,
227− Action: "update",
228− Description: fmt.Sprintf("Update file %s", path),
229− Params: EditPermissionsParams{
230− FilePath: path,
231− Diff: patchDiff,
232− },
233− },
234− )
235− if !p {
236− return ToolResponse{}, permission.ErrorPermissionDenied
237− }
238− case diff.ActionDelete:
239− dir := filepath.Dir(path)
240− patchDiff, _, _ := diff.GenerateDiff(*change.OldContent, "", path)
241− p := p.permissions.Request(
242− permission.CreatePermissionRequest{
243− SessionID: sessionID,
244− Path: dir,
245− ToolName: PatchToolName,
246− Action: "delete",
247− Description: fmt.Sprintf("Delete file %s", path),
248− Params: EditPermissionsParams{
249− FilePath: path,
250− Diff: patchDiff,
251− },
252− },
253− )
254− if !p {
255− return ToolResponse{}, permission.ErrorPermissionDenied
256− }
257− }
258− }
259−
260− // Apply the changes to the filesystem
261− err = diff.ApplyCommit(commit, func(path string, content string) error {
262− absPath := path
263− if !filepath.IsAbs(absPath) {
264− wd := config.WorkingDirectory()
265− absPath = filepath.Join(wd, absPath)
266− }
267−
268− // Create parent directories if needed
269− dir := filepath.Dir(absPath)
270− if err := os.MkdirAll(dir, 0o755); err != nil {
271− return fmt.Errorf("failed to create parent directories for %s: %w", absPath, err)
272− }
273−
274− return os.WriteFile(absPath, []byte(content), 0o644)
275− }, func(path string) error {
276− absPath := path
277− if !filepath.IsAbs(absPath) {
278− wd := config.WorkingDirectory()
279− absPath = filepath.Join(wd, absPath)
280− }
281− return os.Remove(absPath)
282− })
283− if err != nil {
284− return NewTextErrorResponse(fmt.Sprintf("failed to apply patch: %s", err)), nil
285− }
286−
287− // Update file history for all modified files
288− changedFiles := []string{}
289− totalAdditions := 0
290− totalRemovals := 0
291−
292− for path, change := range commit.Changes {
293− absPath := path
294− if !filepath.IsAbs(absPath) {
295− wd := config.WorkingDirectory()
296− absPath = filepath.Join(wd, absPath)
297− }
298− changedFiles = append(changedFiles, absPath)
299−
300− oldContent := ""
301− if change.OldContent != nil {
302− oldContent = *change.OldContent
303− }
304−
305− newContent := ""
306− if change.NewContent != nil {
307− newContent = *change.NewContent
308− }
309−
310− // Calculate diff statistics
311− _, additions, removals := diff.GenerateDiff(oldContent, newContent, path)
312− totalAdditions += additions
313− totalRemovals += removals
314−
315− // Update history
316− file, err := p.files.GetByPathAndSession(ctx, absPath, sessionID)
317− if err != nil && change.Type != diff.ActionAdd {
318− // If not adding a file, create history entry for existing file
319− _, err = p.files.Create(ctx, sessionID, absPath, oldContent)
320− if err != nil {
321− logging.Debug("Error creating file history", "error", err)
322− }
323− }
324−
325− if err == nil && change.Type != diff.ActionAdd && file.Content != oldContent {
326− // User manually changed content, store intermediate version
327− _, err = p.files.CreateVersion(ctx, sessionID, absPath, oldContent)
328− if err != nil {
329− logging.Debug("Error creating file history version", "error", err)
330− }
331− }
332−
333− // Store new version
334− if change.Type == diff.ActionDelete {
335− _, err = p.files.CreateVersion(ctx, sessionID, absPath, "")
336− } else {
337− _, err = p.files.CreateVersion(ctx, sessionID, absPath, newContent)
338− }
339− if err != nil {
340− logging.Debug("Error creating file history version", "error", err)
341− }
342−
343− // Record file operations
344− recordFileWrite(absPath)
345− recordFileRead(absPath)
346− }
347−
348− // Run LSP diagnostics on all changed files
349− for _, filePath := range changedFiles {
350− waitForLspDiagnostics(ctx, filePath, p.lspClients)
351− }
352−
353− result := fmt.Sprintf("Patch applied successfully. %d files changed, %d additions, %d removals",
354− len(changedFiles), totalAdditions, totalRemovals)
355−
356− diagnosticsText := ""
357− for _, filePath := range changedFiles {
358− diagnosticsText += getDiagnostics(filePath, p.lspClients)
359− }
360−
361− if diagnosticsText != "" {
362− result += "\n\nDiagnostics:\n" + diagnosticsText
363− }
364−
365− return WithResponseMetadata(
366− NewTextResponse(result),
367− PatchResponseMetadata{
368− FilesChanged: changedFiles,
369− Additions: totalAdditions,
370− Removals: totalRemovals,
371− }), nil
372−}
internal/tui/components/chat/messages/renderer.go−35
@@ -148,7 +148,6 @@ func init() {
148148 registry.register(tools.GrepToolName, func() renderer { return grepRenderer{} })
149149 registry.register(tools.LSToolName, func() renderer { return lsRenderer{} })
150150 registry.register(tools.SourcegraphToolName, func() renderer { return sourcegraphRenderer{} })
151− registry.register(tools.PatchToolName, func() renderer { return patchRenderer{} })
152151 registry.register(tools.DiagnosticsToolName, func() renderer { return diagnosticsRenderer{} })
153152 registry.register(agent.AgentToolName, func() renderer { return agentRenderer{} })
154153}
@@ -446,38 +445,6 @@ func (sr sourcegraphRenderer) Render(v *toolCallCmp) string {
446445 })
447446}
448447
449−// -----------------------------------------------------------------------------
450−// Patch renderer
451−// -----------------------------------------------------------------------------
452−
453−// patchRenderer handles multi-file patches with change summaries
454−type patchRenderer struct {
455− baseRenderer
456−}
457−
458−// Render displays patch summary with file count and change statistics
459−func (pr patchRenderer) Render(v *toolCallCmp) string {
460− var params tools.PatchParams
461− if err := pr.unmarshalParams(v.call.Input, &params); err != nil {
462− return pr.renderError(v, "Invalid patch parameters")
463− }
464−
465− args := newParamBuilder().addMain("multiple files").build()
466−
467− return pr.renderWithParams(v, "Patch", args, func() string {
468− var meta tools.PatchResponseMetadata
469− if err := pr.unmarshalParams(v.result.Metadata, &meta); err != nil {
470− return renderPlainContent(v, v.result.Content)
471− }
472−
473− summary := fmt.Sprintf("Changed %d files (%d+ %d-)",
474− len(meta.FilesChanged), meta.Additions, meta.Removals)
475− filesList := strings.Join(meta.FilesChanged, "\n")
476−
477− return renderPlainContent(v, summary+"\n\n"+filesList)
478− })
479−}
480−
481448// -----------------------------------------------------------------------------
482449// Diagnostics renderer
483450// -----------------------------------------------------------------------------
@@ -711,8 +678,6 @@ func prettifyToolName(name string) string {
711678 return "View"
712679 case tools.WriteToolName:
713680 return "Write"
714− case tools.PatchToolName:
715− return "Patch"
716681 default:
717682 return name
718683 }
internal/tui/components/dialogs/permissions/permissions.go−16
@@ -279,20 +279,6 @@ func (p *permissionDialogCmp) renderEditContent() string {
279279 return ""
280280}
281281
282−func (p *permissionDialogCmp) renderPatchContent() string {
283− if pr, ok := p.permission.Params.(tools.EditPermissionsParams); ok {
284− diff := p.GetOrSetDiff(p.permission.ID, func() (string, error) {
285− return diff.FormatDiff(pr.Diff, diff.WithTotalWidth(p.contentViewPort.Width()))
286− })
287−
288− contentHeight := min(p.height-9, lipgloss.Height(diff))
289− p.contentViewPort.SetHeight(contentHeight)
290− p.contentViewPort.SetContent(diff)
291− return p.styleViewport()
292− }
293− return ""
294−}
295−
296282func (p *permissionDialogCmp) renderWriteContent() string {
297283 if pr, ok := p.permission.Params.(tools.WritePermissionsParams); ok {
298284 // Use the cache for diff rendering
@@ -381,8 +367,6 @@ func (p *permissionDialogCmp) render() string {
381367 contentFinal = p.renderBashContent()
382368 case tools.EditToolName:
383369 contentFinal = p.renderEditContent()
384− case tools.PatchToolName:
385− contentFinal = p.renderPatchContent()
386370 case tools.WriteToolName:
387371 contentFinal = p.renderWriteContent()
388372 case tools.FetchToolName:
internal/tui/tui.go−11
@@ -8,7 +8,6 @@ import (
88 "github.com/charmbracelet/crush/internal/app"
99 "github.com/charmbracelet/crush/internal/config"
1010 "github.com/charmbracelet/crush/internal/llm/agent"
11− "github.com/charmbracelet/crush/internal/llm/tools"
1211 "github.com/charmbracelet/crush/internal/logging"
1312 "github.com/charmbracelet/crush/internal/permission"
1413 "github.com/charmbracelet/crush/internal/pubsub"
@@ -220,16 +219,6 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
220219 return a, tea.Batch(cmds...)
221220 // Key Press Messages
222221 case tea.KeyPressMsg:
223− if msg.String() == "ctrl+t" {
224− go a.app.Permissions.Request(permission.CreatePermissionRequest{
225− SessionID: "123",
226− ToolName: "bash",
227− Action: "execute",
228− Params: tools.BashPermissionsParams{
229− Command: "ls -la",
230− },
231− })
232− }
233222 return a, a.handleKeyPressMsg(msg)
234223 }
235224 s, _ := a.status.Update(msg)