14 "github.com/charmbracelet/crush/internal/csync"
15 "github.com/charmbracelet/crush/internal/diff"
16 "github.com/charmbracelet/crush/internal/filepathext"
17 "github.com/charmbracelet/crush/internal/fsext"
18 "github.com/charmbracelet/crush/internal/history"
20 "github.com/charmbracelet/crush/internal/lsp"
21 "github.com/charmbracelet/crush/internal/permission"
25var writeDescription []byte
27type WriteParams struct {
28 FilePath string `json:"file_path" description:"The path to the file to write"`
29 Content string `json:"content" description:"The content to write to the file"`
32type WritePermissionsParams struct {
33 FilePath string `json:"file_path"`
34 OldContent string `json:"old_content,omitempty"`
35 NewContent string `json:"new_content,omitempty"`
38type writeTool struct {
39 lspClients *csync.Map[string, *lsp.Client]
40 permissions permission.Service
45type WriteResponseMetadata struct {
46 Diff string `json:"diff"`
47 Additions int `json:"additions"`
48 Removals int `json:"removals"`
51const WriteToolName = "write"
53func NewWriteTool(lspClients *csync.Map[string, *lsp.Client], permissions permission.Service, files history.Service, workingDir string) fantasy.AgentTool {
54 return fantasy.NewAgentTool(
56 string(writeDescription),
57 func(ctx context.Context, params WriteParams, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
58 if params.FilePath == "" {
59 return fantasy.NewTextErrorResponse("file_path is required"), nil
62 if params.Content == "" {
63 return fantasy.NewTextErrorResponse("content is required"), nil
66 filePath := filepathext.SmartJoin(workingDir, params.FilePath)
68 fileInfo, err := os.Stat(filePath)
71 return fantasy.NewTextErrorResponse(fmt.Sprintf("Path is a directory, not a file: %s", filePath)), nil
74 modTime := fileInfo.ModTime()
75 lastRead := getLastReadTime(filePath)
76 if modTime.After(lastRead) {
77 return fantasy.NewTextErrorResponse(fmt.Sprintf("File %s has been modified since it was last read.\nLast modification: %s\nLast read: %s\n\nPlease read the file again before modifying it.",
78 filePath, modTime.Format(time.RFC3339), lastRead.Format(time.RFC3339))), nil
81 oldContent, readErr := os.ReadFile(filePath)
82 if readErr == nil && string(oldContent) == params.Content {
83 return fantasy.NewTextErrorResponse(fmt.Sprintf("File %s already contains the exact content. No changes made.", filePath)), nil
85 } else if !os.IsNotExist(err) {
86 return fantasy.ToolResponse{}, fmt.Errorf("error checking file: %w", err)
89 dir := filepath.Dir(filePath)
90 if err = os.MkdirAll(dir, 0o755); err != nil {
91 return fantasy.ToolResponse{}, fmt.Errorf("error creating directory: %w", err)
95 if fileInfo != nil && !fileInfo.IsDir() {
96 oldBytes, readErr := os.ReadFile(filePath)
98 oldContent = string(oldBytes)
102 sessionID := GetSessionFromContext(ctx)
104 return fantasy.ToolResponse{}, fmt.Errorf("session_id is required")
107 diff, additions, removals := diff.GenerateDiff(
110 strings.TrimPrefix(filePath, workingDir),
113 p := permissions.Request(
114 permission.CreatePermissionRequest{
115 SessionID: sessionID,
116 Path: fsext.PathOrPrefix(filePath, workingDir),
118 ToolName: WriteToolName,
120 Description: fmt.Sprintf("Create file %s", filePath),
121 Params: WritePermissionsParams{
123 OldContent: oldContent,
124 NewContent: params.Content,
129 return fantasy.ToolResponse{}, permission.ErrorPermissionDenied
132 err = os.WriteFile(filePath, []byte(params.Content), 0o644)
134 return fantasy.ToolResponse{}, fmt.Errorf("error writing file: %w", err)
137 // Check if file exists in history
138 file, err := files.GetByPathAndSession(ctx, filePath, sessionID)
140 _, err = files.Create(ctx, sessionID, filePath, oldContent)
142 // Log error but don't fail the operation
143 return fantasy.ToolResponse{}, fmt.Errorf("error creating file history: %w", err)
146 if file.Content != oldContent {
147 // User Manually changed the content store an intermediate version
148 _, err = files.CreateVersion(ctx, sessionID, filePath, oldContent)
150 slog.Error("Error creating file history version", "error", err)
153 // Store the new version
154 _, err = files.CreateVersion(ctx, sessionID, filePath, params.Content)
156 slog.Error("Error creating file history version", "error", err)
159 recordFileWrite(filePath)
160 recordFileRead(filePath)
162 notifyLSPs(ctx, lspClients, params.FilePath)
164 result := fmt.Sprintf("File successfully written: %s", filePath)
165 result = fmt.Sprintf("<result>\n%s\n</result>", result)
166 result += getDiagnostics(filePath, lspClients)
167 return fantasy.WithResponseMetadata(fantasy.NewTextResponse(result),
168 WriteResponseMetadata{
170 Additions: additions,
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list