@@ -10,11 +10,11 @@ import (
1010 "strings"
1111 "time"
1212
13+ "charm.land/fantasy"
1314 "github.com/charmbracelet/crush/internal/csync"
1415 "github.com/charmbracelet/crush/internal/diff"
1516 "github.com/charmbracelet/crush/internal/fsext"
1617 "github.com/charmbracelet/crush/internal/history"
17− "github.com/charmbracelet/fantasy/ai"
1818
1919 "github.com/charmbracelet/crush/internal/lsp"
2020 "github.com/charmbracelet/crush/internal/permission"
@@ -52,20 +52,20 @@ type editContext struct {
5252 workingDir string
5353}
5454
55−func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) ai.AgentTool {
56− return ai.NewAgentTool(
55+func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) fantasy.AgentTool {
56+ return fantasy.NewAgentTool(
5757 EditToolName,
5858 string(editDescription),
59− func(ctx context.Context, params EditParams, call ai.ToolCall) (ai.ToolResponse, error) {
59+ func(ctx context.Context, params EditParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
6060 if params.FilePath == "" {
61− return ai.NewTextErrorResponse("file_path is required"), nil
61+ return fantasy.NewTextErrorResponse("file_path is required"), nil
6262 }
6363
6464 if !filepath.IsAbs(params.FilePath) {
6565 params.FilePath = filepath.Join(workingDir, params.FilePath)
6666 }
6767
68− var response ai.ToolResponse
68+ var response fantasy.ToolResponse
6969 var err error
7070
7171 editCtx := editContext{ctx, permissions, files, workingDir}
@@ -103,25 +103,25 @@ func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
103103 })
104104}
105105
106−func createNewFile(edit editContext, filePath, content string, call ai.ToolCall) (ai.ToolResponse, error) {
106+func createNewFile(edit editContext, filePath, content string, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
107107 fileInfo, err := os.Stat(filePath)
108108 if err == nil {
109109 if fileInfo.IsDir() {
110− return ai.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
110+ return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
111111 }
112− return ai.NewTextErrorResponse(fmt.Sprintf("file already exists: %s", filePath)), nil
112+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file already exists: %s", filePath)), nil
113113 } else if !os.IsNotExist(err) {
114− return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
114+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
115115 }
116116
117117 dir := filepath.Dir(filePath)
118118 if err = os.MkdirAll(dir, 0o755); err != nil {
119− return ai.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
119+ return fantasy.ToolResponse{}, fmt.Errorf("failed to create parent directories: %w", err)
120120 }
121121
122122 sessionID := GetSessionFromContext(edit.ctx)
123123 if sessionID == "" {
124− return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
124+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
125125 }
126126
127127 _, additions, removals := diff.GenerateDiff(
@@ -145,19 +145,19 @@ func createNewFile(edit editContext, filePath, content string, call ai.ToolCall)
145145 },
146146 )
147147 if !p {
148− return ai.ToolResponse{}, permission.ErrorPermissionDenied
148+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
149149 }
150150
151151 err = os.WriteFile(filePath, []byte(content), 0o644)
152152 if err != nil {
153− return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
153+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
154154 }
155155
156156 // File can't be in the history so we create a new file history
157157 _, err = edit.files.Create(edit.ctx, sessionID, filePath, "")
158158 if err != nil {
159159 // Log error but don't fail the operation
160− return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
160+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
161161 }
162162
163163 // Add the new content to the file history
@@ -170,8 +170,8 @@ func createNewFile(edit editContext, filePath, content string, call ai.ToolCall)
170170 recordFileWrite(filePath)
171171 recordFileRead(filePath)
172172
173− return ai.WithResponseMetadata(
174− ai.NewTextResponse("File created: "+filePath),
173+ return fantasy.WithResponseMetadata(
174+ fantasy.NewTextResponse("File created: "+filePath),
175175 EditResponseMetadata{
176176 OldContent: "",
177177 NewContent: content,
@@ -181,27 +181,27 @@ func createNewFile(edit editContext, filePath, content string, call ai.ToolCall)
181181 ), nil
182182}
183183
184−func deleteContent(edit editContext, filePath, oldString string, replaceAll bool, call ai.ToolCall) (ai.ToolResponse, error) {
184+func deleteContent(edit editContext, filePath, oldString string, replaceAll bool, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
185185 fileInfo, err := os.Stat(filePath)
186186 if err != nil {
187187 if os.IsNotExist(err) {
188− return ai.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
188+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
189189 }
190− return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
190+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
191191 }
192192
193193 if fileInfo.IsDir() {
194− return ai.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
194+ return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
195195 }
196196
197197 if getLastReadTime(filePath).IsZero() {
198− return ai.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
198+ return fantasy.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
199199 }
200200
201201 modTime := fileInfo.ModTime()
202202 lastRead := getLastReadTime(filePath)
203203 if modTime.After(lastRead) {
204− return ai.NewTextErrorResponse(
204+ return fantasy.NewTextErrorResponse(
205205 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
206206 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
207207 )), nil
@@ -209,7 +209,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
209209
210210 content, err := os.ReadFile(filePath)
211211 if err != nil {
212− return ai.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
212+ return fantasy.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
213213 }
214214
215215 oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
@@ -221,17 +221,17 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
221221 newContent = strings.ReplaceAll(oldContent, oldString, "")
222222 deletionCount = strings.Count(oldContent, oldString)
223223 if deletionCount == 0 {
224− return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
224+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
225225 }
226226 } else {
227227 index := strings.Index(oldContent, oldString)
228228 if index == -1 {
229− return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
229+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
230230 }
231231
232232 lastIndex := strings.LastIndex(oldContent, oldString)
233233 if index != lastIndex {
234− return ai.NewTextErrorResponse("old_string appears multiple times in the file. Please provide more context to ensure a unique match, or set replace_all to true"), nil
234+ return fantasy.NewTextErrorResponse("old_string appears multiple times in the file. Please provide more context to ensure a unique match, or set replace_all to true"), nil
235235 }
236236
237237 newContent = oldContent[:index] + oldContent[index+len(oldString):]
@@ -241,7 +241,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
241241 sessionID := GetSessionFromContext(edit.ctx)
242242
243243 if sessionID == "" {
244− return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
244+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
245245 }
246246
247247 _, additions, removals := diff.GenerateDiff(
@@ -266,7 +266,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
266266 },
267267 )
268268 if !p {
269− return ai.ToolResponse{}, permission.ErrorPermissionDenied
269+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
270270 }
271271
272272 if isCrlf {
@@ -275,7 +275,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
275275
276276 err = os.WriteFile(filePath, []byte(newContent), 0o644)
277277 if err != nil {
278− return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
278+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
279279 }
280280
281281 // Check if file exists in history
@@ -284,7 +284,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
284284 _, err = edit.files.Create(edit.ctx, sessionID, filePath, oldContent)
285285 if err != nil {
286286 // Log error but don't fail the operation
287− return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
287+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
288288 }
289289 }
290290 if file.Content != oldContent {
@@ -303,8 +303,8 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
303303 recordFileWrite(filePath)
304304 recordFileRead(filePath)
305305
306− return ai.WithResponseMetadata(
307− ai.NewTextResponse("Content deleted from file: "+filePath),
306+ return fantasy.WithResponseMetadata(
307+ fantasy.NewTextResponse("Content deleted from file: "+filePath),
308308 EditResponseMetadata{
309309 OldContent: oldContent,
310310 NewContent: newContent,
@@ -314,27 +314,27 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
314314 ), nil
315315}
316316
317−func replaceContent(edit editContext, filePath, oldString, newString string, replaceAll bool, call ai.ToolCall) (ai.ToolResponse, error) {
317+func replaceContent(edit editContext, filePath, oldString, newString string, replaceAll bool, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
318318 fileInfo, err := os.Stat(filePath)
319319 if err != nil {
320320 if os.IsNotExist(err) {
321− return ai.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
321+ return fantasy.NewTextErrorResponse(fmt.Sprintf("file not found: %s", filePath)), nil
322322 }
323− return ai.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
323+ return fantasy.ToolResponse{}, fmt.Errorf("failed to access file: %w", err)
324324 }
325325
326326 if fileInfo.IsDir() {
327− return ai.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
327+ return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
328328 }
329329
330330 if getLastReadTime(filePath).IsZero() {
331− return ai.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
331+ return fantasy.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
332332 }
333333
334334 modTime := fileInfo.ModTime()
335335 lastRead := getLastReadTime(filePath)
336336 if modTime.After(lastRead) {
337− return ai.NewTextErrorResponse(
337+ return fantasy.NewTextErrorResponse(
338338 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
339339 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339),
340340 )), nil
@@ -342,7 +342,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
342342
343343 content, err := os.ReadFile(filePath)
344344 if err != nil {
345− return ai.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
345+ return fantasy.ToolResponse{}, fmt.Errorf("failed to read file: %w", err)
346346 }
347347
348348 oldContent, isCrlf := fsext.ToUnixLineEndings(string(content))
@@ -354,17 +354,17 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
354354 newContent = strings.ReplaceAll(oldContent, oldString, newString)
355355 replacementCount = strings.Count(oldContent, oldString)
356356 if replacementCount == 0 {
357− return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
357+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
358358 }
359359 } else {
360360 index := strings.Index(oldContent, oldString)
361361 if index == -1 {
362− return ai.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
362+ return fantasy.NewTextErrorResponse("old_string not found in file. Make sure it matches exactly, including whitespace and line breaks"), nil
363363 }
364364
365365 lastIndex := strings.LastIndex(oldContent, oldString)
366366 if index != lastIndex {
367− return ai.NewTextErrorResponse("old_string appears multiple times in the file. Please provide more context to ensure a unique match, or set replace_all to true"), nil
367+ return fantasy.NewTextErrorResponse("old_string appears multiple times in the file. Please provide more context to ensure a unique match, or set replace_all to true"), nil
368368 }
369369
370370 newContent = oldContent[:index] + newString + oldContent[index+len(oldString):]
@@ -372,12 +372,12 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
372372 }
373373
374374 if oldContent == newContent {
375− return ai.NewTextErrorResponse("new content is the same as old content. No changes made."), nil
375+ return fantasy.NewTextErrorResponse("new content is the same as old content. No changes made."), nil
376376 }
377377 sessionID := GetSessionFromContext(edit.ctx)
378378
379379 if sessionID == "" {
380− return ai.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
380+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
381381 }
382382 _, additions, removals := diff.GenerateDiff(
383383 oldContent,
@@ -401,7 +401,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
401401 },
402402 )
403403 if !p {
404− return ai.ToolResponse{}, permission.ErrorPermissionDenied
404+ return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
405405 }
406406
407407 if isCrlf {
@@ -410,7 +410,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
410410
411411 err = os.WriteFile(filePath, []byte(newContent), 0o644)
412412 if err != nil {
413− return ai.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
413+ return fantasy.ToolResponse{}, fmt.Errorf("failed to write file: %w", err)
414414 }
415415
416416 // Check if file exists in history
@@ -419,7 +419,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
419419 _, err = edit.files.Create(edit.ctx, sessionID, filePath, oldContent)
420420 if err != nil {
421421 // Log error but don't fail the operation
422− return ai.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
422+ return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
423423 }
424424 }
425425 if file.Content != oldContent {
@@ -438,8 +438,8 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
438438 recordFileWrite(filePath)
439439 recordFileRead(filePath)
440440
441− return ai.WithResponseMetadata(
442− ai.NewTextResponse("Content replaced in file: "+filePath),
441+ return fantasy.WithResponseMetadata(
442+ fantasy.NewTextResponse("Content replaced in file: "+filePath),
443443 EditResponseMetadata{
444444 OldContent: oldContent,
445445 NewContent: newContent,