2025-03-25 20:04:36 +08:00
package tools
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
2025-04-21 20:29:03 +08:00
"strings"
2025-03-25 20:04:36 +08:00
"time"
2025-04-25 00:25:52 +08:00
"github.com/opencode-ai/opencode/internal/config"
"github.com/opencode-ai/opencode/internal/diff"
"github.com/opencode-ai/opencode/internal/history"
"github.com/opencode-ai/opencode/internal/logging"
"github.com/opencode-ai/opencode/internal/lsp"
"github.com/opencode-ai/opencode/internal/permission"
2025-03-25 20:04:36 +08:00
)
type WriteParams struct {
FilePath string ` json:"file_path" `
Content string ` json:"content" `
}
2025-03-28 05:35:48 +08:00
type WritePermissionsParams struct {
FilePath string ` json:"file_path" `
2025-04-13 00:45:36 +08:00
Diff string ` json:"diff" `
2025-03-28 05:35:48 +08:00
}
2025-04-09 01:15:23 +08:00
type writeTool struct {
lspClients map [ string ] * lsp . Client
permissions permission . Service
2025-04-17 02:06:23 +08:00
files history . Service
2025-04-09 01:15:23 +08:00
}
2025-04-13 00:45:36 +08:00
type WriteResponseMetadata struct {
2025-04-14 17:24:36 +08:00
Diff string ` json:"diff" `
Additions int ` json:"additions" `
Removals int ` json:"removals" `
2025-04-13 00:45:36 +08:00
}
2025-04-09 01:15:23 +08:00
const (
WriteToolName = "write"
writeDescription = ` File writing tool that creates or updates files in the filesystem , allowing you to save or modify text content .
WHEN TO USE THIS TOOL :
- Use when you need to create a new file
- Helpful for updating existing files with modified content
- Perfect for saving generated code , configurations , or text data
HOW TO USE :
- Provide the path to the file you want to write
- Include the content to be written to the file
- The tool will create any necessary parent directories
FEATURES :
- Can create new files or overwrite existing ones
- Creates parent directories automatically if they don ' t exist
- Checks if the file has been modified since last read for safety
- Avoids unnecessary writes when content hasn ' t changed
LIMITATIONS :
- You should read a file before writing to it to avoid conflicts
- Cannot append to files ( rewrites the entire file )
TIPS :
- Use the View tool first to examine existing files before modifying them
- Use the LS tool to verify the correct location when creating new files
- Combine with Glob and Grep tools to find and modify multiple files
- Always include descriptive comments when making changes to existing code `
)
2025-04-17 02:06:23 +08:00
func NewWriteTool ( lspClients map [ string ] * lsp . Client , permissions permission . Service , files history . Service ) BaseTool {
2025-04-09 01:15:23 +08:00
return & writeTool {
lspClients : lspClients ,
permissions : permissions ,
2025-04-17 02:06:23 +08:00
files : files ,
2025-04-09 01:15:23 +08:00
}
}
2025-03-28 05:35:48 +08:00
func ( w * writeTool ) Info ( ) ToolInfo {
return ToolInfo {
Name : WriteToolName ,
2025-04-09 01:15:23 +08:00
Description : writeDescription ,
2025-03-28 05:35:48 +08:00
Parameters : map [ string ] any {
"file_path" : map [ string ] any {
"type" : "string" ,
"description" : "The path to the file to write" ,
2025-03-25 20:04:36 +08:00
} ,
2025-03-28 05:35:48 +08:00
"content" : map [ string ] any {
"type" : "string" ,
"description" : "The content to write to the file" ,
2025-03-25 20:04:36 +08:00
} ,
2025-03-28 05:35:48 +08:00
} ,
Required : [ ] string { "file_path" , "content" } ,
}
2025-03-25 20:04:36 +08:00
}
2025-03-28 05:35:48 +08:00
func ( w * writeTool ) Run ( ctx context . Context , call ToolCall ) ( ToolResponse , error ) {
2025-03-25 20:04:36 +08:00
var params WriteParams
2025-03-28 05:35:48 +08:00
if err := json . Unmarshal ( [ ] byte ( call . Input ) , & params ) ; err != nil {
return NewTextErrorResponse ( fmt . Sprintf ( "error parsing parameters: %s" , err ) ) , nil
2025-03-25 20:04:36 +08:00
}
if params . FilePath == "" {
2025-03-28 05:35:48 +08:00
return NewTextErrorResponse ( "file_path is required" ) , nil
}
if params . Content == "" {
return NewTextErrorResponse ( "content is required" ) , nil
2025-03-25 20:04:36 +08:00
}
2025-03-28 05:35:48 +08:00
filePath := params . FilePath
if ! filepath . IsAbs ( filePath ) {
filePath = filepath . Join ( config . WorkingDirectory ( ) , filePath )
2025-03-25 20:04:36 +08:00
}
2025-03-28 05:35:48 +08:00
fileInfo , err := os . Stat ( filePath )
2025-03-25 20:04:36 +08:00
if err == nil {
if fileInfo . IsDir ( ) {
2025-03-28 05:35:48 +08:00
return NewTextErrorResponse ( fmt . Sprintf ( "Path is a directory, not a file: %s" , filePath ) ) , nil
2025-03-25 20:04:36 +08:00
}
modTime := fileInfo . ModTime ( )
2025-03-28 05:35:48 +08:00
lastRead := getLastReadTime ( filePath )
2025-03-25 20:04:36 +08:00
if modTime . After ( lastRead ) {
2025-03-28 05:35:48 +08:00
return 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." ,
filePath , modTime . Format ( time . RFC3339 ) , lastRead . Format ( time . RFC3339 ) ) ) , nil
2025-03-25 20:04:36 +08:00
}
2025-03-28 05:35:48 +08:00
oldContent , readErr := os . ReadFile ( filePath )
if readErr == nil && string ( oldContent ) == params . Content {
return NewTextErrorResponse ( fmt . Sprintf ( "File %s already contains the exact content. No changes made." , filePath ) ) , nil
}
2025-03-25 20:04:36 +08:00
} else if ! os . IsNotExist ( err ) {
2025-04-14 17:24:36 +08:00
return ToolResponse { } , fmt . Errorf ( "error checking file: %w" , err )
2025-03-25 20:04:36 +08:00
}
2025-03-28 05:35:48 +08:00
dir := filepath . Dir ( filePath )
if err = os . MkdirAll ( dir , 0 o755 ) ; err != nil {
2025-04-14 17:24:36 +08:00
return ToolResponse { } , fmt . Errorf ( "error creating directory: %w" , err )
2025-03-28 05:35:48 +08:00
}
2025-04-03 21:20:15 +08:00
2025-04-04 20:36:57 +08:00
oldContent := ""
if fileInfo != nil && ! fileInfo . IsDir ( ) {
oldBytes , readErr := os . ReadFile ( filePath )
if readErr == nil {
oldContent = string ( oldBytes )
}
}
2025-04-09 01:15:23 +08:00
2025-04-13 20:37:05 +08:00
sessionID , messageID := GetContextValues ( ctx )
2025-04-13 00:45:36 +08:00
if sessionID == "" || messageID == "" {
2025-04-14 17:24:36 +08:00
return ToolResponse { } , fmt . Errorf ( "session_id and message_id are required" )
2025-04-13 00:45:36 +08:00
}
2025-04-14 20:09:17 +08:00
diff , additions , removals := diff . GenerateDiff (
2025-04-13 00:45:36 +08:00
oldContent ,
params . Content ,
2025-04-14 20:09:17 +08:00
filePath ,
2025-04-13 00:45:36 +08:00
)
2025-04-21 20:29:03 +08:00
rootDir := config . WorkingDirectory ( )
permissionPath := filepath . Dir ( filePath )
if strings . HasPrefix ( filePath , rootDir ) {
permissionPath = rootDir
}
2025-04-09 01:15:23 +08:00
p := w . permissions . Request (
2025-03-25 20:04:36 +08:00
permission . CreatePermissionRequest {
2025-04-22 01:48:36 +08:00
SessionID : sessionID ,
2025-04-21 20:29:03 +08:00
Path : permissionPath ,
2025-03-25 20:04:36 +08:00
ToolName : WriteToolName ,
2025-04-21 20:29:03 +08:00
Action : "write" ,
2025-03-28 05:35:48 +08:00
Description : fmt . Sprintf ( "Create file %s" , filePath ) ,
Params : WritePermissionsParams {
FilePath : filePath ,
2025-04-13 00:45:36 +08:00
Diff : diff ,
2025-03-25 20:04:36 +08:00
} ,
} ,
)
if ! p {
2025-04-14 17:24:36 +08:00
return ToolResponse { } , permission . ErrorPermissionDenied
2025-03-25 20:04:36 +08:00
}
2025-03-28 05:35:48 +08:00
err = os . WriteFile ( filePath , [ ] byte ( params . Content ) , 0 o644 )
2025-03-25 20:04:36 +08:00
if err != nil {
2025-04-14 17:24:36 +08:00
return ToolResponse { } , fmt . Errorf ( "error writing file: %w" , err )
2025-03-25 20:04:36 +08:00
}
2025-04-17 02:06:23 +08:00
// Check if file exists in history
file , err := w . files . GetByPathAndSession ( ctx , filePath , sessionID )
if err != nil {
_ , err = w . files . Create ( ctx , sessionID , filePath , oldContent )
if err != nil {
// Log error but don't fail the operation
return ToolResponse { } , fmt . Errorf ( "error creating file history: %w" , err )
}
}
if file . Content != oldContent {
// User Manually changed the content store an intermediate version
_ , err = w . files . CreateVersion ( ctx , sessionID , filePath , oldContent )
if err != nil {
2025-04-21 19:46:32 +08:00
logging . Debug ( "Error creating file history version" , "error" , err )
2025-04-17 02:06:23 +08:00
}
}
// Store the new version
_ , err = w . files . CreateVersion ( ctx , sessionID , filePath , params . Content )
if err != nil {
2025-04-21 19:46:32 +08:00
logging . Debug ( "Error creating file history version" , "error" , err )
2025-04-17 02:06:23 +08:00
}
2025-03-28 05:35:48 +08:00
recordFileWrite ( filePath )
recordFileRead ( filePath )
2025-04-04 21:03:50 +08:00
waitForLspDiagnostics ( ctx , filePath , w . lspClients )
2025-03-25 20:04:36 +08:00
2025-04-03 21:20:15 +08:00
result := fmt . Sprintf ( "File successfully written: %s" , filePath )
result = fmt . Sprintf ( "<result>\n%s\n</result>" , result )
2025-04-14 17:08:17 +08:00
result += getDiagnostics ( filePath , w . lspClients )
2025-04-13 00:45:36 +08:00
return WithResponseMetadata ( NewTextResponse ( result ) ,
WriteResponseMetadata {
2025-04-14 17:24:36 +08:00
Diff : diff ,
2025-04-14 20:09:17 +08:00
Additions : additions ,
Removals : removals ,
2025-04-13 00:45:36 +08:00
} ,
) , nil
2025-03-25 20:04:36 +08:00
}