@@ -56,10 +56,17 @@ type editContext struct {
56 ctx context.Context
56 ctx context.Context
57 permissions permission.Service
57 permissions permission.Service
58 files history.Service
58 files history.Service
59+ filetracker filetracker.Service
59 workingDir string
60 workingDir string
62−func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) fantasy.AgentTool {
64+ lspClients *csync.Map[string, *lsp.Client],
65+ permissions permission.Service,
66+ files history.Service,
67+ filetracker filetracker.Service,
63 return fantasy.NewAgentTool(
70 return fantasy.NewAgentTool(
64 EditToolName,
71 EditToolName,
65 string(editDescription),
72 string(editDescription),
@@ -73,7 +80,7 @@ func NewEditTool(lspClients *csync.Map[string, *lsp.Client], permissions permiss
73 var response fantasy.ToolResponse
80 var response fantasy.ToolResponse
74 var err error
81 var err error
76− editCtx := editContext{ctx, permissions, files, workingDir}
83+ editCtx := editContext{ctx, permissions, files, filetracker, workingDir}
78 if params.OldString == "" {
85 if params.OldString == "" {
79 response, err = createNewFile(editCtx, params.FilePath, params.NewString, call)
86 response, err = createNewFile(editCtx, params.FilePath, params.NewString, call)
@@ -168,8 +175,7 @@ func createNewFile(edit editContext, filePath, content string, call fantasy.Tool
168 slog.Error("Error creating file history version", "error", err)
175 slog.Error("Error creating file history version", "error", err)
171− filetracker.RecordWrite(filePath)
172− filetracker.RecordRead(filePath)
178+ edit.filetracker.RecordRead(edit.ctx, sessionID, filePath)
174 return fantasy.WithResponseMetadata(
180 return fantasy.WithResponseMetadata(
175 fantasy.NewTextResponse("File created: "+filePath),
181 fantasy.NewTextResponse("File created: "+filePath),
@@ -195,12 +201,17 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
195 return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
201 return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
204+ sessionID := GetSessionFromContext(edit.ctx)
205+ if sessionID == "" {
206+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for deleting content")
198− if filetracker.LastReadTime(filePath).IsZero() {
209+ lastRead := edit.filetracker.LastReadTime(edit.ctx, sessionID, filePath)
210+ if lastRead.IsZero() {
199 return fantasy.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
211 return fantasy.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
202− modTime := fileInfo.ModTime()
214+ modTime := fileInfo.ModTime().Truncate(time.Second)
203− lastRead := filetracker.LastReadTime(filePath)
204 if modTime.After(lastRead) {
215 if modTime.After(lastRead) {
205 return fantasy.NewTextErrorResponse(
216 return fantasy.NewTextErrorResponse(
206 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
217 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
@@ -236,12 +247,6 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
236 newContent = oldContent[:index] + oldContent[index+len(oldString):]
247 newContent = oldContent[:index] + oldContent[index+len(oldString):]
239− sessionID := GetSessionFromContext(edit.ctx)
241− if sessionID == "" {
242− return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for deleting content")
245 _, additions, removals := diff.GenerateDiff(
250 _, additions, removals := diff.GenerateDiff(
246 oldContent,
251 oldContent,
247 newContent,
252 newContent,
@@ -301,8 +306,7 @@ func deleteContent(edit editContext, filePath, oldString string, replaceAll bool
301 slog.Error("Error creating file history version", "error", err)
306 slog.Error("Error creating file history version", "error", err)
304− filetracker.RecordWrite(filePath)
305− filetracker.RecordRead(filePath)
309+ edit.filetracker.RecordRead(edit.ctx, sessionID, filePath)
307 return fantasy.WithResponseMetadata(
311 return fantasy.WithResponseMetadata(
308 fantasy.NewTextResponse("Content deleted from file: "+filePath),
312 fantasy.NewTextResponse("Content deleted from file: "+filePath),
@@ -328,12 +332,17 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
328 return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
332 return fantasy.NewTextErrorResponse(fmt.Sprintf("path is a directory, not a file: %s", filePath)), nil
335+ sessionID := GetSessionFromContext(edit.ctx)
336+ if sessionID == "" {
337+ return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for edit a file")
331− if filetracker.LastReadTime(filePath).IsZero() {
340+ lastRead := edit.filetracker.LastReadTime(edit.ctx, sessionID, filePath)
341+ if lastRead.IsZero() {
332 return fantasy.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
342 return fantasy.NewTextErrorResponse("you must read the file before editing it. Use the View tool first"), nil
335− modTime := fileInfo.ModTime()
345+ modTime := fileInfo.ModTime().Truncate(time.Second)
336− lastRead := filetracker.LastReadTime(filePath)
337 if modTime.After(lastRead) {
346 if modTime.After(lastRead) {
338 return fantasy.NewTextErrorResponse(
347 return fantasy.NewTextErrorResponse(
339 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
348 fmt.Sprintf("file %s has been modified since it was last read (mod time: %s, last read: %s)",
@@ -369,11 +378,6 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
369 if oldContent == newContent {
378 if oldContent == newContent {
370 return fantasy.NewTextErrorResponse("new content is the same as old content. No changes made."), nil
379 return fantasy.NewTextErrorResponse("new content is the same as old content. No changes made."), nil
372− sessionID := GetSessionFromContext(edit.ctx)
374− if sessionID == "" {
375− return fantasy.ToolResponse{}, fmt.Errorf("session ID is required for creating a new file")
377 _, additions, removals := diff.GenerateDiff(
381 _, additions, removals := diff.GenerateDiff(
378 oldContent,
382 oldContent,
379 newContent,
383 newContent,
@@ -433,8 +437,7 @@ func replaceContent(edit editContext, filePath, oldString, newString string, rep
433 slog.Error("Error creating file history version", "error", err)
437 slog.Error("Error creating file history version", "error", err)
436− filetracker.RecordWrite(filePath)
437− filetracker.RecordRead(filePath)
440+ edit.filetracker.RecordRead(edit.ctx, sessionID, filePath)
439 return fantasy.WithResponseMetadata(
442 return fantasy.WithResponseMetadata(
440 fantasy.NewTextResponse("Content replaced in file: "+filePath),
443 fantasy.NewTextResponse("Content replaced in file: "+filePath),